diff --git a/README.md b/README.md index e0538da9..511a6858 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,8 @@ _instructions for google oauth2 setup borrowed from [oauth2_proxy](https://githu #### Initial import from Disqus 1. Disqus provides an export of all comments on your site in a g-zipped file. This is found in your Moderation panel at Disqus Admin > Setup > Export. The export will be sent into a queue and then emailed to the address associated with your account once it's ready. Direct link to export will be something like `https://.disqus.com/admin/discussions/export/`. See [importing-exporting](https://help.disqus.com/customer/portal/articles/1104797-importing-exporting) for more details. -2. Move this file to your remark42 host within `.var` and unzip, i.e. `gunzip .xml.gz`. -3. Run import command - `docker-compose exec remark /srv/import-disqus.sh .xml ` +2. Move this file to your remark42 host within `./var` and unzip, i.e. `gunzip .xml.gz`. +3. Run import command - `docker-compose exec remark42 /srv/import-disqus.sh .xml ` #### Backup and restore @@ -132,7 +132,6 @@ In addition to automatic backups user can make a backup manually. This command m Backup file is a text file with all exported comments separated by EOL. Each backup record is a valid json with all key/value unmarshaled from `Comment` struct (see below). - #### Admin users Admins/moderators should be defined in `docker-compose.yml` as a list of user IDs or passed in the command line. diff --git a/app/rest/api/rest.go b/app/rest/api/rest.go index bb88c64a..bcc0d4e8 100644 --- a/app/rest/api/rest.go +++ b/app/rest/api/rest.go @@ -157,6 +157,7 @@ func (s *Rest) routes() chi.Router { rauth.Get("/user", s.userInfoCtrl) rauth.Put("/vote/{id}", s.voteCtrl) rauth.Get("/userdata", s.userAllDataCtrl) + rauth.Post("/deleteme", s.deleteMeCtrl) // admin routes, admin users only rauth.Mount("/admin", s.adminService.routes(s.Authenticator.AdminOnly, Logger(nil, LogAll))) diff --git a/app/rest/api/rest_private.go b/app/rest/api/rest_private.go index 92a68b9a..86ceb510 100644 --- a/app/rest/api/rest_private.go +++ b/app/rest/api/rest_private.go @@ -10,11 +10,13 @@ import ( "strings" "time" + jwt "github.com/dgrijalva/jwt-go" "github.com/go-chi/chi" "github.com/go-chi/render" blackfriday "gopkg.in/russross/blackfriday.v2" "github.com/umputun/remark/app/rest" + "github.com/umputun/remark/app/rest/auth" "github.com/umputun/remark/app/store" "github.com/umputun/remark/app/store/service" ) @@ -211,3 +213,30 @@ func (s *Rest) userAllDataCtrl(w http.ResponseWriter, r *http.Request) { } } } + +// POST /deleteme?site_id=site +func (s *Rest) deleteMeCtrl(w http.ResponseWriter, r *http.Request) { + user, err := rest.GetUserInfo(r) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") + return + } + siteID := r.URL.Query().Get("site") + + claims := auth.CustomClaims{ + SiteID: siteID, + StandardClaims: jwt.StandardClaims{ + Issuer: "remark42", + ExpiresAt: time.Now().AddDate(0, 3, 0).Unix(), + NotBefore: time.Now().Add(-1 * time.Minute).Unix(), + }, + User: &user, + } + + tokenStr, err := s.Authenticator.JWTService.Token(&claims) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't make token") + return + } + render.JSON(w, r, JSON{"site": siteID, "user_id": user.ID, "token": tokenStr}) +} diff --git a/app/rest/api/rest_private_test.go b/app/rest/api/rest_private_test.go new file mode 100644 index 00000000..0deed380 --- /dev/null +++ b/app/rest/api/rest_private_test.go @@ -0,0 +1,263 @@ +package api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/umputun/remark/app/store" +) + +func TestRest_Create(t *testing.T) { + srv, ts := prep(t) + require.NotNil(t, srv) + defer cleanup(ts) + + resp, err := post(t, ts.URL+"/api/v1/comment", + `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) + assert.Nil(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) + + b, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + c := JSON{} + err = json.Unmarshal(b, &c) + assert.Nil(t, err) + loc := c["locator"].(map[string]interface{}) + assert.Equal(t, "radio-t", loc["site"]) + assert.Equal(t, "https://radio-t.com/blah1", loc["url"]) + assert.True(t, len(c["id"].(string)) > 8) +} + +func TestRest_CreateOldPost(t *testing.T) { + srv, ts := prep(t) + require.NotNil(t, srv) + defer cleanup(ts) + + // make old, but not too old comment + old := store.Comment{Text: "test test old", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5), + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} + _, err := srv.DataService.Create(old) + assert.Nil(t, err) + + comments, err := srv.DataService.Find(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, "time") + assert.Nil(t, err) + assert.Equal(t, 1, len(comments)) + + // try to add new comment to the same old post + resp, err := post(t, ts.URL+"/api/v1/comment", + `{"text": "test 123", "locator":{"site": "radio-t","url": "https://radio-t.com/blah1"}}`) + assert.Nil(t, err) + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + assert.Nil(t, srv.DataService.DeleteAll("radio-t")) + // make too old comment + old = store.Comment{Text: "test test old", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -15), + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} + _, err = srv.DataService.Create(old) + assert.Nil(t, err) + + resp, err = post(t, ts.URL+"/api/v1/comment", + `{"text": "test 123", "locator":{"site": "radio-t","url": "https://radio-t.com/blah1"}}`) + assert.Nil(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode) +} + +func TestRest_CreateTooBig(t *testing.T) { + srv, ts := prep(t) + require.NotNil(t, srv) + defer cleanup(ts) + + longComment := fmt.Sprintf(`{"text": "%4001s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, "Щ") + + resp, err := post(t, ts.URL+"/api/v1/comment", longComment) + assert.Nil(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + b, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + c := JSON{} + err = json.Unmarshal(b, &c) + assert.Nil(t, err) + + assert.Equal(t, "comment text exceeded max allowed size 4000 (4001)", c["error"]) + assert.Equal(t, "invalid comment", c["details"]) +} +func TestRest_CreateAndGet(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + // create comment + resp, err := post(t, ts.URL+"/api/v1/comment", + `{"text": "**test** *123* http://radio-t.com", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) + require.Nil(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode) + b, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + c := JSON{} + err = json.Unmarshal(b, &c) + assert.Nil(t, err) + + id := c["id"].(string) + + // get created comment by id + res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) + assert.Equal(t, 200, code) + comment := store.Comment{} + err = json.Unmarshal([]byte(res), &comment) + assert.Nil(t, err) + assert.Equal(t, `

test 123 http://radio-t.com

`+"\n", comment.Text) + assert.Equal(t, "**test** *123* http://radio-t.com", comment.Orig) + assert.Equal(t, store.User{Name: "developer one", ID: "dev", + Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: "dbc7c999343f003f189f70aaf52cc04443f90790"}, + comment.User) + t.Logf("%+v", comment) +} + +func TestRest_Update(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + c1 := store.Comment{Text: "test test #1", ParentID: "p1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} + id := addComment(t, c1, ts) + + client := http.Client{} + req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1", + strings.NewReader(`{"text":"updated text", "summary":"my edit"}`)) + assert.Nil(t, err) + req = withBasicAuth(req, "dev", "password") + b, err := client.Do(req) + assert.Nil(t, err) + body, err := ioutil.ReadAll(b.Body) + assert.Nil(t, err) + assert.Equal(t, 200, b.StatusCode, string(body)) + + // comments returned by update + c2 := store.Comment{} + err = json.Unmarshal(body, &c2) + assert.Nil(t, err) + assert.Equal(t, id, c2.ID) + assert.Equal(t, "

updated text

\n", c2.Text) + assert.Equal(t, "updated text", c2.Orig) + assert.Equal(t, "my edit", c2.Edit.Summary) + assert.True(t, time.Since(c2.Edit.Timestamp) < 1*time.Second) + + // read updated comment + res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) + assert.Equal(t, 200, code) + c3 := store.Comment{} + err = json.Unmarshal([]byte(res), &c3) + assert.Nil(t, err) + assert.Equal(t, c2, c3, "same as response from update") +} + +func TestRest_UpdateNotOwner(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + c1 := store.Comment{Text: "test test #1", ParentID: "p1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "xyz"}} + id1, err := srv.DataService.Create(c1) + assert.Nil(t, err) + + client := http.Client{} + req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+ + "?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`{"text":"updated text", "summary":"my edit"}`)) + assert.Nil(t, err) + req = withBasicAuth(req, "dev", "password") + b, err := client.Do(req) + assert.Nil(t, err) + body, err := ioutil.ReadAll(b.Body) + assert.Nil(t, err) + assert.Equal(t, 403, b.StatusCode, string(body), "update from non-owner") + assert.Equal(t, `{"details":"can not edit comments for other users","error":"rejected"}`+"\n", string(body)) + + client = http.Client{} + req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+ + "?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`ERRR "text":"updated text", "summary":"my"}`)) + assert.Nil(t, err) + req = withBasicAuth(req, "dev", "password") + b, err = client.Do(req) + assert.Nil(t, err) + assert.Equal(t, 400, b.StatusCode, string(body), "update is not json") +} + +func TestRest_Vote(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + c1 := store.Comment{Text: "test test #1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} + c2 := store.Comment{Text: "test test #2", ParentID: "p1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} + + id1 := addComment(t, c1, ts) + addComment(t, c2, ts) + + vote := func(val int) int { + client := http.Client{} + req, err := http.NewRequest(http.MethodPut, + fmt.Sprintf("%s/api/v1/vote/%s?site=radio-t&url=https://radio-t.com/blah&vote=%d", ts.URL, id1, val), nil) + assert.Nil(t, err) + req = withBasicAuth(req, "dev", "password") + resp, err := client.Do(req) + assert.Nil(t, err) + return resp.StatusCode + } + + assert.Equal(t, 200, vote(1), "first vote allowed") + assert.Equal(t, 400, vote(1), "second vote rejected") + body, code := get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) + assert.Equal(t, 200, code) + cr := store.Comment{} + err := json.Unmarshal([]byte(body), &cr) + assert.Nil(t, err) + assert.Equal(t, 1, cr.Score) + assert.Equal(t, map[string]bool{"dev": true}, cr.Votes) + + assert.Equal(t, 200, vote(-1), "opposite vote allowed") + body, code = get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) + assert.Equal(t, 200, code) + cr = store.Comment{} + err = json.Unmarshal([]byte(body), &cr) + assert.Nil(t, err) + assert.Equal(t, 0, cr.Score) + assert.Equal(t, map[string]bool{}, cr.Votes) +} + +func TestRest_DeleteMe(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + client := http.Client{} + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/deleteme?site=radio-t", ts.URL), nil) + assert.Nil(t, err) + req = withBasicAuth(req, "dev", "password") + resp, err := client.Do(req) + assert.Nil(t, err) + body, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + + m := map[string]string{} + err = json.Unmarshal(body, &m) + assert.Nil(t, err) + assert.Equal(t, "radio-t", m["site"]) + assert.Equal(t, "dev", m["user_id"]) + + token := m["token"] + claims, err := srv.Authenticator.JWTService.Parse(token) + assert.Nil(t, err) + assert.Equal(t, "dev", claims.User.ID) +} diff --git a/app/rest/api/rest_public_test.go b/app/rest/api/rest_public_test.go new file mode 100644 index 00000000..b83ca349 --- /dev/null +++ b/app/rest/api/rest_public_test.go @@ -0,0 +1,465 @@ +package api + +import ( + "compress/gzip" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/umputun/remark/app/rest" + "github.com/umputun/remark/app/store" +) + +func TestRest_Ping(t *testing.T) { + srv, ts := prep(t) + require.NotNil(t, srv) + defer cleanup(ts) + + res, code := get(t, ts.URL+"/api/v1/ping") + assert.Equal(t, "pong", res) + assert.Equal(t, 200, code) +} + +func TestRest_Preview(t *testing.T) { + srv, ts := prep(t) + require.NotNil(t, srv) + defer cleanup(ts) + + resp, err := post(t, ts.URL+"/api/v1/preview", `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + b, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + assert.Equal(t, "

test 123

\n", string(b)) +} + +func TestRest_PreviewWithMD(t *testing.T) { + srv, ts := prep(t) + require.NotNil(t, srv) + defer cleanup(ts) + + text := ` +# h1 + +BKT +func TestRest_Preview(t *testing.T) { +srv, ts := prep(t) + require.NotNil(t, srv) +} +BKT +` + text = strings.Replace(text, "BKT", "```", -1) + j := fmt.Sprintf(`{"text": "%s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, text) + j = strings.Replace(j, "\n", "\\n", -1) + t.Log(j) + + resp, err := post(t, ts.URL+"/api/v1/preview", j) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + b, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + assert.Equal(t, "

h1

\n\n
func TestRest_Preview(t *testing.T) {\nsrv, ts := prep(t)\n  require.NotNil(t, srv)\n}\n
\n", string(b)) +} + +func TestRest_Find(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + _, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1") + assert.Equal(t, 400, code, "nothing in") + + c1 := store.Comment{Text: "test test #1", ParentID: "", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} + id1 := addComment(t, c1, ts) + + c2 := store.Comment{Text: "test test #2", ParentID: id1, + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} + id2 := addComment(t, c2, ts) + + assert.NotEqual(t, id1, id2) + + // get sorted by +time + res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&sort=+time") + assert.Equal(t, 200, code) + comments := []store.Comment{} + err := json.Unmarshal([]byte(res), &comments) + assert.Nil(t, err) + assert.Equal(t, 2, len(comments), "should have 2 comments") + assert.Equal(t, id1, comments[0].ID) + assert.Equal(t, id2, comments[1].ID) + + // get sorted by -time + res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&sort=-time") + assert.Equal(t, 200, code) + err = json.Unmarshal([]byte(res), &comments) + assert.Nil(t, err) + assert.Equal(t, 2, len(comments), "should have 2 comments") + assert.Equal(t, id1, comments[1].ID) + assert.Equal(t, id2, comments[0].ID) + + // get in tree mode + tree := rest.Tree{} + res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&format=tree") + assert.Equal(t, 200, code) + err = json.Unmarshal([]byte(res), &tree) + assert.Nil(t, err) + assert.Equal(t, 1, len(tree.Nodes)) + assert.Equal(t, 1, len(tree.Nodes[0].Replies)) + assert.Equal(t, 2, tree.Info.Count) + assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL) + assert.False(t, tree.Info.ReadOnly, "post is fresh") +} + +func TestRest_FindAge(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5), + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} + _, err := srv.DataService.Create(c1) + require.Nil(t, err) + + c2 := store.Comment{Text: "test test #2", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -15), + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}, User: store.User{ID: "u1"}} + _, err = srv.DataService.Create(c2) + require.Nil(t, err) + + tree := rest.Tree{} + + res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&format=tree") + assert.Equal(t, 200, code) + err = json.Unmarshal([]byte(res), &tree) + assert.Nil(t, err) + assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL) + assert.False(t, tree.Info.ReadOnly, "post is fresh") + + res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah2&format=tree") + assert.Equal(t, 200, code) + err = json.Unmarshal([]byte(res), &tree) + assert.Nil(t, err) + assert.Equal(t, "https://radio-t.com/blah2", tree.Info.URL) + assert.True(t, tree.Info.ReadOnly, "post is old") +} + +func TestRest_FindReadOnly(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -1), + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} + _, err := srv.DataService.Create(c1) + + require.Nil(t, err) + + c2 := store.Comment{Text: "test test #2", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -2), + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}, User: store.User{ID: "u1"}} + _, err = srv.DataService.Create(c2) + require.Nil(t, err) + + // set post to read-only + client := http.Client{} + req, err := http.NewRequest(http.MethodPut, + fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah1&ro=1", ts.URL), nil) + assert.Nil(t, err) + withBasicAuth(req, "dev", "password") + _, err = client.Do(req) + require.Nil(t, err) + + tree := rest.Tree{} + res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&format=tree") + assert.Equal(t, 200, code) + err = json.Unmarshal([]byte(res), &tree) + require.Nil(t, err) + assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL) + assert.True(t, tree.Info.ReadOnly, "post is ro") + + tree = rest.Tree{} + res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah2&format=tree") + assert.Equal(t, 200, code) + err = json.Unmarshal([]byte(res), &tree) + require.Nil(t, err) + assert.Equal(t, "https://radio-t.com/blah2", tree.Info.URL) + assert.False(t, tree.Info.ReadOnly, "post is writable") +} + +func TestRest_Last(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + c1 := store.Comment{Text: "test test #1", ParentID: "p1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} + c2 := store.Comment{Text: "test test #2", ParentID: "p1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} + + // add 3 comments + addComment(t, c1, ts) + id1 := addComment(t, c1, ts) + id2 := addComment(t, c2, ts) + + res, code := get(t, ts.URL+"/api/v1/last/2?site=radio-t") + assert.Equal(t, 200, code) + comments := []store.Comment{} + err := json.Unmarshal([]byte(res), &comments) + assert.Nil(t, err) + assert.Equal(t, 2, len(comments), "should have 2 comments") + assert.Equal(t, id1, comments[1].ID) + assert.Equal(t, id2, comments[0].ID) + + res, code = get(t, ts.URL+"/api/v1/last/5?site=radio-t") + assert.Equal(t, 200, code) + err = json.Unmarshal([]byte(res), &comments) + assert.Nil(t, err) + assert.Equal(t, 3, len(comments), "should have 3 comments") + + res, code = get(t, ts.URL+"/api/v1/last/X?site=radio-t") + assert.Equal(t, 200, code) + err = json.Unmarshal([]byte(res), &comments) + assert.Nil(t, err) + assert.Equal(t, 3, len(comments), "should have 3 comments") + + err = srv.DataService.Delete(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, id1, store.SoftDelete) + assert.Nil(t, err) + res, code = get(t, ts.URL+"/api/v1/last/5?site=radio-t") + assert.Equal(t, 200, code) + err = json.Unmarshal([]byte(res), &comments) + assert.Nil(t, err) + assert.Equal(t, 2, len(comments), "should have 2 comments") +} + +func TestRest_FindUserComments(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + c1 := store.Comment{Text: "test test #1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} + c2 := store.Comment{Text: "test test #3", ParentID: "p1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} + + // add 3 comments + addComment(t, c1, ts) + addComment(t, c2, ts) + addComment(t, c2, ts) + + _, code := get(t, ts.URL+"/api/v1/comments?site=radio-t&user=blah") + assert.Equal(t, 400, code, "noting for user blah") + + res, code := get(t, ts.URL+"/api/v1/comments?site=radio-t&user=dev") + assert.Equal(t, 200, code) + + resp := struct { + Comments []store.Comment + Count int + }{} + + err := json.Unmarshal([]byte(res), &resp) + assert.Nil(t, err) + assert.Equal(t, 3, len(resp.Comments), "should have 3 comments") + assert.Equal(t, 3, resp.Count, "should have 3 count") +} + +func TestRest_UserInfo(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + body, code := getWithAuth(t, ts.URL+"/api/v1/user?site=radio-t") + assert.Equal(t, 200, code) + user := store.User{} + err := json.Unmarshal([]byte(body), &user) + assert.Nil(t, err) + assert.Equal(t, store.User{Name: "developer one", ID: "dev", + Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: ""}, user) +} + +func TestRest_Count(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + c1 := store.Comment{Text: "test test #1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} + c2 := store.Comment{Text: "test test #2", ParentID: "p1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} + + addComment(t, c1, ts) + addComment(t, c1, ts) + addComment(t, c1, ts) + addComment(t, c2, ts) + addComment(t, c2, ts) + + body, code := get(t, ts.URL+"/api/v1/count?site=radio-t&url=https://radio-t.com/blah1") + assert.Equal(t, 200, code) + j := JSON{} + err := json.Unmarshal([]byte(body), &j) + assert.Nil(t, err) + assert.Equal(t, 3.0, j["count"]) + + body, code = get(t, ts.URL+"/api/v1/count?site=radio-t&url=https://radio-t.com/blah2") + assert.Equal(t, 200, code) + err = json.Unmarshal([]byte(body), &j) + assert.Nil(t, err) + assert.Equal(t, 2.0, j["count"]) +} + +func TestRest_Counts(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + c1 := store.Comment{Text: "test test #1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} + c2 := store.Comment{Text: "test test #2", ParentID: "p1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} + + addComment(t, c1, ts) + addComment(t, c1, ts) + addComment(t, c1, ts) + addComment(t, c2, ts) + addComment(t, c2, ts) + + resp, err := post(t, ts.URL+"/api/v1/counts?site=radio-t", `["https://radio-t.com/blah1","https://radio-t.com/blah2"]`) + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + body, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + + j := []store.PostInfo{} + err = json.Unmarshal(body, &j) + assert.Nil(t, err) + assert.Equal(t, []store.PostInfo([]store.PostInfo{{URL: "https://radio-t.com/blah1", Count: 3}, + {URL: "https://radio-t.com/blah2", Count: 2}}), j) +} + +func TestRest_List(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + c1 := store.Comment{Text: "test test #1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} + c2 := store.Comment{Text: "test test #2", ParentID: "p1", + Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} + + addComment(t, c1, ts) + addComment(t, c1, ts) + addComment(t, c1, ts) + addComment(t, c2, ts) + addComment(t, c2, ts) + + body, code := get(t, ts.URL+"/api/v1/list?site=radio-t") + assert.Equal(t, 200, code) + pi := []store.PostInfo{} + err := json.Unmarshal([]byte(body), &pi) + assert.Nil(t, err) + assert.Equal(t, "https://radio-t.com/blah2", pi[0].URL) + assert.Equal(t, 2, pi[0].Count) + assert.Equal(t, "https://radio-t.com/blah1", pi[1].URL) + assert.Equal(t, 3, pi[1].Count) +} + +func TestRest_Config(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + body, code := get(t, ts.URL+"/api/v1/config?site=radio-t") + assert.Equal(t, 200, code) + j := JSON{} + err := json.Unmarshal([]byte(body), &j) + assert.Nil(t, err) + assert.Equal(t, 300., j["edit_duration"]) + assert.EqualValues(t, []interface{}([]interface{}{"a1", "a2"}), j["admins"]) + assert.Equal(t, 4000., j["max_comment_size"]) + assert.Equal(t, -5., j["low_score"]) + assert.Equal(t, -10., j["critical_score"]) + assert.Equal(t, 10., j["readonly_age"]) + t.Logf("%+v", j) +} + +func TestRest_Info(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + user := store.User{ID: "user1", Name: "user name 1"} + c1 := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", + URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 10, 0, time.Local)} + c2 := store.Comment{User: user, Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", + URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 20, 0, time.Local)} + c3 := store.Comment{User: user, Text: "test test #3", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", + URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 25, 0, time.Local)} + + _, err := srv.DataService.Create(c1) + require.Nil(t, err, "%+v", err) + _, err = srv.DataService.Create(c2) + require.Nil(t, err) + _, err = srv.DataService.Create(c3) + require.Nil(t, err) + + body, code := get(t, ts.URL+"/api/v1/info?site=radio-t&url=https://radio-t.com/blah1") + assert.Equal(t, 200, code) + + info := store.PostInfo{} + err = json.Unmarshal([]byte(body), &info) + assert.Nil(t, err) + exp := store.PostInfo{URL: "https://radio-t.com/blah1", Count: 3, + FirstTS: time.Date(2018, 05, 27, 1, 14, 10, 0, time.Local), LastTS: time.Date(2018, 05, 27, 1, 14, 25, 0, time.Local)} + assert.Equal(t, exp, info) + + _, code = get(t, ts.URL+"/api/v1/info?site=radio-t&url=https://radio-t.com/blah-no") + assert.Equal(t, 400, code) + _, code = get(t, ts.URL+"/api/v1/info?site=radio-t-no&url=https://radio-t.com/blah-no") + assert.Equal(t, 400, code) +} + +func TestRest_UserAllData(t *testing.T) { + srv, ts := prep(t) + assert.NotNil(t, srv) + defer cleanup(ts) + + // write 3 comments + user := store.User{ID: "dev", Name: "user name 1"} + c1 := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", + URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 10, 0, time.Local)} + c2 := store.Comment{User: user, Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", + URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 20, 0, time.Local)} + c3 := store.Comment{User: user, Text: "test test #3", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", + URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 25, 0, time.Local)} + _, err := srv.DataService.Create(c1) + require.Nil(t, err, "%+v", err) + _, err = srv.DataService.Create(c2) + require.Nil(t, err) + _, err = srv.DataService.Create(c3) + require.Nil(t, err) + + client := &http.Client{Timeout: 1 * time.Second} + req, err := http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil) + require.Nil(t, err) + req = withBasicAuth(req, "dev", "password") + resp, err := client.Do(req) + require.Nil(t, err) + require.Equal(t, 200, resp.StatusCode) + require.Equal(t, "application/gzip", resp.Header.Get("Content-Type")) + + ungzReader, err := gzip.NewReader(resp.Body) + assert.NoError(t, err) + ungzBody, err := ioutil.ReadAll(ungzReader) + assert.NoError(t, err) + assert.True(t, strings.HasPrefix(string(ungzBody), + `{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","admin":true}[`)) + assert.Equal(t, 3, strings.Count(string(ungzBody), `"text":`), "3 comments inside") + t.Logf("%s", string(ungzBody)) +} diff --git a/app/rest/api/rest_test.go b/app/rest/api/rest_test.go index 39e1127e..184560b7 100644 --- a/app/rest/api/rest_test.go +++ b/app/rest/api/rest_test.go @@ -2,10 +2,8 @@ package api import ( "bytes" - "compress/gzip" "encoding/base64" "encoding/json" - "fmt" "io/ioutil" "net/http" "net/http/httptest" @@ -19,7 +17,6 @@ import ( "github.com/stretchr/testify/require" "github.com/umputun/remark/app/migrator" - "github.com/umputun/remark/app/rest" "github.com/umputun/remark/app/rest/auth" "github.com/umputun/remark/app/rest/proxy" "github.com/umputun/remark/app/store" @@ -30,675 +27,6 @@ import ( var testDb = "/tmp/test-remark.db" var testHTML = "/tmp/test-remark.html" -func TestRest_Ping(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts) - - res, code := get(t, ts.URL+"/api/v1/ping") - assert.Equal(t, "pong", res) - assert.Equal(t, 200, code) -} - -func TestRest_Create(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts) - - resp, err := post(t, ts.URL+"/api/v1/comment", - `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) - assert.Nil(t, err) - require.Equal(t, http.StatusCreated, resp.StatusCode) - - b, err := ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - c := JSON{} - err = json.Unmarshal(b, &c) - assert.Nil(t, err) - loc := c["locator"].(map[string]interface{}) - assert.Equal(t, "radio-t", loc["site"]) - assert.Equal(t, "https://radio-t.com/blah1", loc["url"]) - assert.True(t, len(c["id"].(string)) > 8) -} - -func TestRest_CreateOldPost(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts) - - // make old, but not too old comment - old := store.Comment{Text: "test test old", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5), - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} - _, err := srv.DataService.Create(old) - assert.Nil(t, err) - - comments, err := srv.DataService.Find(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, "time") - assert.Nil(t, err) - assert.Equal(t, 1, len(comments)) - - // try to add new comment to the same old post - resp, err := post(t, ts.URL+"/api/v1/comment", - `{"text": "test 123", "locator":{"site": "radio-t","url": "https://radio-t.com/blah1"}}`) - assert.Nil(t, err) - assert.Equal(t, http.StatusCreated, resp.StatusCode) - - assert.Nil(t, srv.DataService.DeleteAll("radio-t")) - // make too old comment - old = store.Comment{Text: "test test old", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -15), - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} - _, err = srv.DataService.Create(old) - assert.Nil(t, err) - - resp, err = post(t, ts.URL+"/api/v1/comment", - `{"text": "test 123", "locator":{"site": "radio-t","url": "https://radio-t.com/blah1"}}`) - assert.Nil(t, err) - assert.Equal(t, http.StatusForbidden, resp.StatusCode) -} - -func TestRest_CreateTooBig(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts) - - longComment := fmt.Sprintf(`{"text": "%4001s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, "Щ") - - resp, err := post(t, ts.URL+"/api/v1/comment", longComment) - assert.Nil(t, err) - assert.Equal(t, http.StatusBadRequest, resp.StatusCode) - b, err := ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - c := JSON{} - err = json.Unmarshal(b, &c) - assert.Nil(t, err) - - assert.Equal(t, "comment text exceeded max allowed size 4000 (4001)", c["error"]) - assert.Equal(t, "invalid comment", c["details"]) -} - -func TestRest_Preview(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts) - - resp, err := post(t, ts.URL+"/api/v1/preview", `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) - assert.Nil(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) - b, err := ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - assert.Equal(t, "

test 123

\n", string(b)) -} - -func TestRest_PreviewWithMD(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts) - - text := ` -# h1 - -BKT -func TestRest_Preview(t *testing.T) { -srv, ts := prep(t) - require.NotNil(t, srv) -} -BKT -` - text = strings.Replace(text, "BKT", "```", -1) - j := fmt.Sprintf(`{"text": "%s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, text) - j = strings.Replace(j, "\n", "\\n", -1) - t.Log(j) - - resp, err := post(t, ts.URL+"/api/v1/preview", j) - assert.Nil(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) - b, err := ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - assert.Equal(t, "

h1

\n\n
func TestRest_Preview(t *testing.T) {\nsrv, ts := prep(t)\n  require.NotNil(t, srv)\n}\n
\n", string(b)) -} - -func TestRest_CreateAndGet(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - // create comment - resp, err := post(t, ts.URL+"/api/v1/comment", - `{"text": "**test** *123* http://radio-t.com", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) - require.Nil(t, err) - require.Equal(t, http.StatusCreated, resp.StatusCode) - b, err := ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - c := JSON{} - err = json.Unmarshal(b, &c) - assert.Nil(t, err) - - id := c["id"].(string) - - // get created comment by id - res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) - assert.Equal(t, 200, code) - comment := store.Comment{} - err = json.Unmarshal([]byte(res), &comment) - assert.Nil(t, err) - assert.Equal(t, `

test 123 http://radio-t.com

`+"\n", comment.Text) - assert.Equal(t, "**test** *123* http://radio-t.com", comment.Orig) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", - Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: "dbc7c999343f003f189f70aaf52cc04443f90790"}, - comment.User) - t.Logf("%+v", comment) -} - -func TestRest_Find(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - _, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1") - assert.Equal(t, 400, code, "nothing in") - - c1 := store.Comment{Text: "test test #1", ParentID: "", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - id1 := addComment(t, c1, ts) - - c2 := store.Comment{Text: "test test #2", ParentID: id1, - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - id2 := addComment(t, c2, ts) - - assert.NotEqual(t, id1, id2) - - // get sorted by +time - res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&sort=+time") - assert.Equal(t, 200, code) - comments := []store.Comment{} - err := json.Unmarshal([]byte(res), &comments) - assert.Nil(t, err) - assert.Equal(t, 2, len(comments), "should have 2 comments") - assert.Equal(t, id1, comments[0].ID) - assert.Equal(t, id2, comments[1].ID) - - // get sorted by -time - res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&sort=-time") - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(res), &comments) - assert.Nil(t, err) - assert.Equal(t, 2, len(comments), "should have 2 comments") - assert.Equal(t, id1, comments[1].ID) - assert.Equal(t, id2, comments[0].ID) - - // get in tree mode - tree := rest.Tree{} - res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&format=tree") - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(res), &tree) - assert.Nil(t, err) - assert.Equal(t, 1, len(tree.Nodes)) - assert.Equal(t, 1, len(tree.Nodes[0].Replies)) - assert.Equal(t, 2, tree.Info.Count) - assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL) - assert.False(t, tree.Info.ReadOnly, "post is fresh") -} - -func TestRest_FindAge(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5), - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} - _, err := srv.DataService.Create(c1) - require.Nil(t, err) - - c2 := store.Comment{Text: "test test #2", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -15), - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}, User: store.User{ID: "u1"}} - _, err = srv.DataService.Create(c2) - require.Nil(t, err) - - tree := rest.Tree{} - - res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&format=tree") - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(res), &tree) - assert.Nil(t, err) - assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL) - assert.False(t, tree.Info.ReadOnly, "post is fresh") - - res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah2&format=tree") - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(res), &tree) - assert.Nil(t, err) - assert.Equal(t, "https://radio-t.com/blah2", tree.Info.URL) - assert.True(t, tree.Info.ReadOnly, "post is old") -} - -func TestRest_FindReadOnly(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -1), - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} - _, err := srv.DataService.Create(c1) - - require.Nil(t, err) - - c2 := store.Comment{Text: "test test #2", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -2), - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}, User: store.User{ID: "u1"}} - _, err = srv.DataService.Create(c2) - require.Nil(t, err) - - // set post to read-only - client := http.Client{} - req, err := http.NewRequest(http.MethodPut, - fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah1&ro=1", ts.URL), nil) - assert.Nil(t, err) - withBasicAuth(req, "dev", "password") - _, err = client.Do(req) - require.Nil(t, err) - - tree := rest.Tree{} - res, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1&format=tree") - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(res), &tree) - require.Nil(t, err) - assert.Equal(t, "https://radio-t.com/blah1", tree.Info.URL) - assert.True(t, tree.Info.ReadOnly, "post is ro") - - tree = rest.Tree{} - res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah2&format=tree") - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(res), &tree) - require.Nil(t, err) - assert.Equal(t, "https://radio-t.com/blah2", tree.Info.URL) - assert.False(t, tree.Info.ReadOnly, "post is writable") -} - -func TestRest_Update(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - c1 := store.Comment{Text: "test test #1", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - id := addComment(t, c1, ts) - - client := http.Client{} - req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1", - strings.NewReader(`{"text":"updated text", "summary":"my edit"}`)) - assert.Nil(t, err) - req = withBasicAuth(req, "dev", "password") - b, err := client.Do(req) - assert.Nil(t, err) - body, err := ioutil.ReadAll(b.Body) - assert.Nil(t, err) - assert.Equal(t, 200, b.StatusCode, string(body)) - - // comments returned by update - c2 := store.Comment{} - err = json.Unmarshal(body, &c2) - assert.Nil(t, err) - assert.Equal(t, id, c2.ID) - assert.Equal(t, "

updated text

\n", c2.Text) - assert.Equal(t, "updated text", c2.Orig) - assert.Equal(t, "my edit", c2.Edit.Summary) - assert.True(t, time.Since(c2.Edit.Timestamp) < 1*time.Second) - - // read updated comment - res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) - assert.Equal(t, 200, code) - c3 := store.Comment{} - err = json.Unmarshal([]byte(res), &c3) - assert.Nil(t, err) - assert.Equal(t, c2, c3, "same as response from update") -} - -func TestRest_UpdateNotOwner(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - c1 := store.Comment{Text: "test test #1", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "xyz"}} - id1, err := srv.DataService.Create(c1) - assert.Nil(t, err) - - client := http.Client{} - req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+ - "?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`{"text":"updated text", "summary":"my edit"}`)) - assert.Nil(t, err) - req = withBasicAuth(req, "dev", "password") - b, err := client.Do(req) - assert.Nil(t, err) - body, err := ioutil.ReadAll(b.Body) - assert.Nil(t, err) - assert.Equal(t, 403, b.StatusCode, string(body), "update from non-owner") - assert.Equal(t, `{"details":"can not edit comments for other users","error":"rejected"}`+"\n", string(body)) - - client = http.Client{} - req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+ - "?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`ERRR "text":"updated text", "summary":"my"}`)) - assert.Nil(t, err) - req = withBasicAuth(req, "dev", "password") - b, err = client.Do(req) - assert.Nil(t, err) - assert.Equal(t, 400, b.StatusCode, string(body), "update is not json") -} - -func TestRest_Last(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - c1 := store.Comment{Text: "test test #1", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} - - // add 3 comments - addComment(t, c1, ts) - id1 := addComment(t, c1, ts) - id2 := addComment(t, c2, ts) - - res, code := get(t, ts.URL+"/api/v1/last/2?site=radio-t") - assert.Equal(t, 200, code) - comments := []store.Comment{} - err := json.Unmarshal([]byte(res), &comments) - assert.Nil(t, err) - assert.Equal(t, 2, len(comments), "should have 2 comments") - assert.Equal(t, id1, comments[1].ID) - assert.Equal(t, id2, comments[0].ID) - - res, code = get(t, ts.URL+"/api/v1/last/5?site=radio-t") - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(res), &comments) - assert.Nil(t, err) - assert.Equal(t, 3, len(comments), "should have 3 comments") - - res, code = get(t, ts.URL+"/api/v1/last/X?site=radio-t") - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(res), &comments) - assert.Nil(t, err) - assert.Equal(t, 3, len(comments), "should have 3 comments") - - err = srv.DataService.Delete(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, id1, store.SoftDelete) - assert.Nil(t, err) - res, code = get(t, ts.URL+"/api/v1/last/5?site=radio-t") - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(res), &comments) - assert.Nil(t, err) - assert.Equal(t, 2, len(comments), "should have 2 comments") -} - -func TestRest_FindUserComments(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - c2 := store.Comment{Text: "test test #3", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} - - // add 3 comments - addComment(t, c1, ts) - addComment(t, c2, ts) - addComment(t, c2, ts) - - _, code := get(t, ts.URL+"/api/v1/comments?site=radio-t&user=blah") - assert.Equal(t, 400, code, "noting for user blah") - - res, code := get(t, ts.URL+"/api/v1/comments?site=radio-t&user=dev") - assert.Equal(t, 200, code) - - resp := struct { - Comments []store.Comment - Count int - }{} - - err := json.Unmarshal([]byte(res), &resp) - assert.Nil(t, err) - assert.Equal(t, 3, len(resp.Comments), "should have 3 comments") - assert.Equal(t, 3, resp.Count, "should have 3 count") -} - -func TestRest_UserInfo(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - body, code := getWithAuth(t, ts.URL+"/api/v1/user?site=radio-t") - assert.Equal(t, 200, code) - user := store.User{} - err := json.Unmarshal([]byte(body), &user) - assert.Nil(t, err) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", - Picture: "/api/v1/avatar/remark.image", Admin: true, Blocked: false, IP: ""}, user) -} - -func TestRest_Vote(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} - - id1 := addComment(t, c1, ts) - addComment(t, c2, ts) - - vote := func(val int) int { - client := http.Client{} - req, err := http.NewRequest(http.MethodPut, - fmt.Sprintf("%s/api/v1/vote/%s?site=radio-t&url=https://radio-t.com/blah&vote=%d", ts.URL, id1, val), nil) - assert.Nil(t, err) - req = withBasicAuth(req, "dev", "password") - resp, err := client.Do(req) - assert.Nil(t, err) - return resp.StatusCode - } - - assert.Equal(t, 200, vote(1), "first vote allowed") - assert.Equal(t, 400, vote(1), "second vote rejected") - body, code := get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) - assert.Equal(t, 200, code) - cr := store.Comment{} - err := json.Unmarshal([]byte(body), &cr) - assert.Nil(t, err) - assert.Equal(t, 1, cr.Score) - assert.Equal(t, map[string]bool{"dev": true}, cr.Votes) - - assert.Equal(t, 200, vote(-1), "opposite vote allowed") - body, code = get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) - assert.Equal(t, 200, code) - cr = store.Comment{} - err = json.Unmarshal([]byte(body), &cr) - assert.Nil(t, err) - assert.Equal(t, 0, cr.Score) - assert.Equal(t, map[string]bool{}, cr.Votes) -} - -func TestRest_Count(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} - - addComment(t, c1, ts) - addComment(t, c1, ts) - addComment(t, c1, ts) - addComment(t, c2, ts) - addComment(t, c2, ts) - - body, code := get(t, ts.URL+"/api/v1/count?site=radio-t&url=https://radio-t.com/blah1") - assert.Equal(t, 200, code) - j := JSON{} - err := json.Unmarshal([]byte(body), &j) - assert.Nil(t, err) - assert.Equal(t, 3.0, j["count"]) - - body, code = get(t, ts.URL+"/api/v1/count?site=radio-t&url=https://radio-t.com/blah2") - assert.Equal(t, 200, code) - err = json.Unmarshal([]byte(body), &j) - assert.Nil(t, err) - assert.Equal(t, 2.0, j["count"]) -} - -func TestRest_Counts(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} - - addComment(t, c1, ts) - addComment(t, c1, ts) - addComment(t, c1, ts) - addComment(t, c2, ts) - addComment(t, c2, ts) - - resp, err := post(t, ts.URL+"/api/v1/counts?site=radio-t", `["https://radio-t.com/blah1","https://radio-t.com/blah2"]`) - assert.Nil(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) - - body, err := ioutil.ReadAll(resp.Body) - assert.Nil(t, err) - - j := []store.PostInfo{} - err = json.Unmarshal(body, &j) - assert.Nil(t, err) - assert.Equal(t, []store.PostInfo([]store.PostInfo{{URL: "https://radio-t.com/blah1", Count: 3}, - {URL: "https://radio-t.com/blah2", Count: 2}}), j) -} - -func TestRest_List(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - c1 := store.Comment{Text: "test test #1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} - c2 := store.Comment{Text: "test test #2", ParentID: "p1", - Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah2"}} - - addComment(t, c1, ts) - addComment(t, c1, ts) - addComment(t, c1, ts) - addComment(t, c2, ts) - addComment(t, c2, ts) - - body, code := get(t, ts.URL+"/api/v1/list?site=radio-t") - assert.Equal(t, 200, code) - pi := []store.PostInfo{} - err := json.Unmarshal([]byte(body), &pi) - assert.Nil(t, err) - assert.Equal(t, "https://radio-t.com/blah2", pi[0].URL) - assert.Equal(t, 2, pi[0].Count) - assert.Equal(t, "https://radio-t.com/blah1", pi[1].URL) - assert.Equal(t, 3, pi[1].Count) -} - -func TestRest_Config(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - body, code := get(t, ts.URL+"/api/v1/config?site=radio-t") - assert.Equal(t, 200, code) - j := JSON{} - err := json.Unmarshal([]byte(body), &j) - assert.Nil(t, err) - assert.Equal(t, 300., j["edit_duration"]) - assert.EqualValues(t, []interface{}([]interface{}{"a1", "a2"}), j["admins"]) - assert.Equal(t, 4000., j["max_comment_size"]) - assert.Equal(t, -5., j["low_score"]) - assert.Equal(t, -10., j["critical_score"]) - assert.Equal(t, 10., j["readonly_age"]) - t.Logf("%+v", j) -} - -func TestRest_Info(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - user := store.User{ID: "user1", Name: "user name 1"} - c1 := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", - URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 10, 0, time.Local)} - c2 := store.Comment{User: user, Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", - URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 20, 0, time.Local)} - c3 := store.Comment{User: user, Text: "test test #3", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", - URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 25, 0, time.Local)} - - _, err := srv.DataService.Create(c1) - require.Nil(t, err, "%+v", err) - _, err = srv.DataService.Create(c2) - require.Nil(t, err) - _, err = srv.DataService.Create(c3) - require.Nil(t, err) - - body, code := get(t, ts.URL+"/api/v1/info?site=radio-t&url=https://radio-t.com/blah1") - assert.Equal(t, 200, code) - - info := store.PostInfo{} - err = json.Unmarshal([]byte(body), &info) - assert.Nil(t, err) - exp := store.PostInfo{URL: "https://radio-t.com/blah1", Count: 3, - FirstTS: time.Date(2018, 05, 27, 1, 14, 10, 0, time.Local), LastTS: time.Date(2018, 05, 27, 1, 14, 25, 0, time.Local)} - assert.Equal(t, exp, info) - - _, code = get(t, ts.URL+"/api/v1/info?site=radio-t&url=https://radio-t.com/blah-no") - assert.Equal(t, 400, code) - _, code = get(t, ts.URL+"/api/v1/info?site=radio-t-no&url=https://radio-t.com/blah-no") - assert.Equal(t, 400, code) -} - -func TestRest_UserAllData(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts) - - // write 3 comments - user := store.User{ID: "dev", Name: "user name 1"} - c1 := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", - URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 10, 0, time.Local)} - c2 := store.Comment{User: user, Text: "test test #2", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", - URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 20, 0, time.Local)} - c3 := store.Comment{User: user, Text: "test test #3", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", - URL: "https://radio-t.com/blah1"}, Timestamp: time.Date(2018, 05, 27, 1, 14, 25, 0, time.Local)} - _, err := srv.DataService.Create(c1) - require.Nil(t, err, "%+v", err) - _, err = srv.DataService.Create(c2) - require.Nil(t, err) - _, err = srv.DataService.Create(c3) - require.Nil(t, err) - - client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil) - require.Nil(t, err) - req = withBasicAuth(req, "dev", "password") - resp, err := client.Do(req) - require.Nil(t, err) - require.Equal(t, 200, resp.StatusCode) - require.Equal(t, "application/gzip", resp.Header.Get("Content-Type")) - - ungzReader, err := gzip.NewReader(resp.Body) - assert.NoError(t, err) - ungzBody, err := ioutil.ReadAll(ungzReader) - assert.NoError(t, err) - assert.True(t, strings.HasPrefix(string(ungzBody), - `{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","admin":true}[`)) - assert.Equal(t, 3, strings.Count(string(ungzBody), `"text":`), "3 comments inside") - t.Logf("%s", string(ungzBody)) -} - func TestRest_FileServer(t *testing.T) { srv, ts := prep(t) assert.NotNil(t, srv) @@ -728,9 +56,10 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) { srv = &Rest{ DataService: dataStore, Authenticator: auth.Authenticator{ - DevPasswd: "password", - Providers: nil, - Admins: []string{"a1", "a2"}, + DevPasswd: "password", + Providers: nil, + Admins: []string{"a1", "a2"}, + JWTService: auth.NewJWT("12345", false, time.Minute), }, Exporter: &migrator.Remark{DataStore: &dataStore}, Cache: &mockCache{}, diff --git a/app/rest/auth/jwt.go b/app/rest/auth/jwt.go index a4b8664f..46f596ad 100644 --- a/app/rest/auth/jwt.go +++ b/app/rest/auth/jwt.go @@ -45,16 +45,45 @@ func NewJWT(secret string, secureCookies bool, exp time.Duration) *JWT { return &res } +// Token makes jwt with claims +func (j *JWT) Token(claims *CustomClaims) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(j.secret)) + if err != nil { + return "", errors.Wrap(err, "can't sign jwt token") + } + return tokenString, nil +} + +// Parse token string and verify +func (j *JWT) Parse(tokenString string) (*CustomClaims, error) { + token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte(j.secret), nil + }) + if err != nil { + return nil, errors.Wrap(err, "can't parse jwt") + } + + claims, ok := token.Claims.(*CustomClaims) + if !ok || !token.Valid { + return nil, errors.New("invalid jwt") + } + return claims, nil +} + // Set creates jwt cookie with xsrf cookie and put it to ResponseWriter // accepts claims and sets expiration if none defined. permanent flag means long-living cookie, false makes it session only. func (j *JWT) Set(w http.ResponseWriter, claims *CustomClaims, sessionOnly bool) error { if claims.ExpiresAt == 0 { claims.ExpiresAt = time.Now().Add(j.exp).Unix() } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - tokenString, err := token.SignedString([]byte(j.secret)) + + tokenString, err := j.Token(claims) if err != nil { - return errors.Wrap(err, "can't sign jwt token") + return errors.Wrap(err, "failed to make jwt token") } cookieExpiration := 0 // session cookie @@ -80,10 +109,12 @@ func (j *JWT) Get(r *http.Request) (*CustomClaims, error) { fromCookie := false tokenString := "" + // try to get from X-JWT header if tokenHeader := r.Header.Get(jwtHeaderKey); tokenHeader != "" { tokenString = tokenHeader } + // try to get from JWT cookie if tokenString == "" { fromCookie = true jc, err := r.Cookie(jwtCookieName) @@ -93,19 +124,9 @@ func (j *JWT) Get(r *http.Request) (*CustomClaims, error) { tokenString = jc.Value } - token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, errors.Errorf("unexpected signing method: %v", token.Header["alg"]) - } - return []byte(j.secret), nil - }) + claims, err := j.Parse(tokenString) if err != nil { - return nil, errors.Wrap(err, "can't parse jwt") - } - - claims, ok := token.Claims.(*CustomClaims) - if !ok || !token.Valid { - return nil, errors.New("invalid jwt") + return nil, errors.Wrap(err, "failed to get jwt") } if fromCookie && claims.User != nil { diff --git a/app/rest/auth/jwt_test.go b/app/rest/auth/jwt_test.go index d057c05d..3c6ed0d7 100644 --- a/app/rest/auth/jwt_test.go +++ b/app/rest/auth/jwt_test.go @@ -26,6 +26,42 @@ var testJwtExpired = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjY4ODc4M "ImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTI2ODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJuYW1lMSIsImlkIjoiaWQxIiwicGljdHVyZSI6IiI" + "sImFkbWluIjpmYWxzZX0sInN0YXRlIjoiMTIzNDU2IiwiZnJvbSI6ImZyb20ifQ.4_dCrY9ihyfZIedz-kZwBTxmxU1a52V7IqeJrOqTzE4" +func TestJWT_Token(t *testing.T) { + j := NewJWT("xyz 12345", false, time.Hour) + + claims := &CustomClaims{ + State: "123456", + From: "from", + User: &store.User{ + ID: "id1", + Name: "name1", + }, + StandardClaims: jwt.StandardClaims{ + Id: "random id", + Issuer: "remark42", + ExpiresAt: time.Date(2058, 5, 21, 1, 30, 22, 0, time.Local).Unix(), + NotBefore: time.Date(2018, 5, 21, 1, 30, 22, 0, time.Local).Unix(), + }, + } + + res, err := j.Token(claims) + assert.Nil(t, err) + assert.Equal(t, testJwtValid, res) +} + +func TestJWT_Parse(t *testing.T) { + j := NewJWT("xyz 12345", false, time.Hour) + claims, err := j.Parse(testJwtValid) + assert.NoError(t, err) + assert.Equal(t, &store.User{Name: "name1", ID: "id1"}, claims.User) + + _, err = j.Parse(testJwtExpired) + assert.NotNil(t, err, "expired token") + + _, err = j.Parse("bad") + assert.NotNil(t, err, "bad token") +} + func TestJWT_Set(t *testing.T) { j := NewJWT("xyz 12345", false, time.Hour) @@ -85,13 +121,13 @@ func TestJWT_GetFromHeader(t *testing.T) { req.Header.Add(jwtHeaderKey, testJwtExpired) _, err = j.Get(req) assert.NotNil(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "can't parse jwt: token is expired by"), err.Error()) + assert.True(t, strings.Contains(err.Error(), "can't parse jwt: token is expired by"), err.Error()) req = httptest.NewRequest("GET", "/", nil) req.Header.Add(jwtHeaderKey, "bad bad token") _, err = j.Get(req) assert.NotNil(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "can't parse jwt: token contains an invalid number of segments"), err.Error()) + assert.True(t, strings.Contains(err.Error(), "can't parse jwt: token contains an invalid number of segments"), err.Error()) } @@ -209,7 +245,7 @@ func TestJWT_SetAndGetWithCookiesExpired(t *testing.T) { req.Header.Add(xsrfHeaderKey, "random id") _, err = j.Get(req) assert.NotNil(t, err) - assert.True(t, strings.HasPrefix(err.Error(), "can't parse jwt: token is expired by"), err.Error()) + assert.True(t, strings.Contains(err.Error(), "can't parse jwt: token is expired by"), err.Error()) } func TestJWT_Refresh(t *testing.T) { diff --git a/docker-compose.yml b/docker-compose.yml index 01c2ef15..5a8e2fb8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: remark: build: . image: umputun/remark:master - container_name: "remark" + container_name: "remark42" hostname: "remark" restart: always