add flash message helper

This commit is contained in:
Henry 2021-04-01 09:04:38 +02:00
parent da62b1eecc
commit cec7bc0e44
34 changed files with 727 additions and 634 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ cmd/insert-user/insert-user
muxrpc/test/go/testrun
muxrpc/test/nodejs/testrun
web/handlers/testrun
web/handlers/admin/testrun
roomdb/sqlite/testrun
# build artifacts from node.js project web/styles

2
go.mod
View File

@ -33,7 +33,7 @@ require (
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59
go.cryptoscope.co/netwrap v0.1.1
go.cryptoscope.co/secretstream v1.2.2
go.mindeco.de v1.10.0
go.mindeco.de v1.11.0
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9

2
go.sum
View File

@ -499,6 +499,8 @@ go.mindeco.de v1.9.0 h1:/xli02DkzpIUZxp/rp1nj8z/OZ9MHvkMIr9TfDVcmBg=
go.mindeco.de v1.9.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de v1.10.0 h1:H/bhL+dIgZZnUgBEDlKUJBisTszNiHDONeGZtGdiJJ0=
go.mindeco.de v1.10.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de v1.11.0 h1:zrNJFjRr7l+eoRywKxBetB1sgQwnrJ9NA65H/xeusTs=
go.mindeco.de v1.11.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870 h1:TCI3AefMAaOYECvppn30+CfEB0Fn8IES1SKvvacc3/c=
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870/go.mod h1:OnBnV02ux4lLsZ39LID6yYLqSDp+dqTHb/3miYPkQFs=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=

View File

@ -34,6 +34,13 @@ func (f ErrForbidden) Error() string {
return fmt.Sprintf("rooms/web: access denied: %s", f.Details)
}
// ErrRedirect decide to not render a page during the controller
type ErrRedirect struct{ Path string }
func (err ErrRedirect) Error() string {
return fmt.Sprintf("rooms/web: redirecting to: %s", err.Path)
}
type PageNotFound struct{ Path string }
func (e PageNotFound) Error() string {

View File

@ -31,7 +31,40 @@ func (eh *ErrorHandler) SetRenderer(r *render.Renderer) {
}
func (eh *ErrorHandler) Handle(rw http.ResponseWriter, req *http.Request, code int, err error) {
var ih = i18n.LocalizerFromRequest(eh.locHelper, req)
var redirectErr ErrRedirect
if errors.As(err, &redirectErr) {
http.Redirect(rw, req, redirectErr.Path, http.StatusTemporaryRedirect)
return
}
var ih = eh.locHelper.FromRequest(req)
code, msg := localizeError(ih, err)
data := errorTemplateData{
Err: template.HTML(msg),
// TODO: localize status codes? might be fine with a few
Status: http.StatusText(code),
StatusCode: code,
}
renderErr := eh.render.Render(rw, req, "error.tmpl", code, data)
if renderErr != nil {
logger := logging.FromContext(req.Context())
level.Error(logger).Log("event", "error template renderfailed",
"orig-err", err,
"render-err", renderErr,
)
}
}
type errorTemplateData struct {
StatusCode int
Status string
Err template.HTML
}
func localizeError(ih *i18n.Localizer, err error) (int, string) {
// default, unlocalized message
msg := err.Error()
@ -44,10 +77,13 @@ func (eh *ErrorHandler) Handle(rw http.ResponseWriter, req *http.Request, code i
f ErrForbidden
)
code := http.StatusInternalServerError
switch {
case err == ErrNotAuthorized:
code = http.StatusForbidden
msg = ih.LocalizeSimple("ErrorAuthBadLogin")
msg = ih.LocalizeSimple("ErrorNotAuthorized")
case err == auth.ErrBadLogin:
msg = ih.LocalizeSimple("ErrorAuthBadLogin")
@ -83,25 +119,5 @@ func (eh *ErrorHandler) Handle(rw http.ResponseWriter, req *http.Request, code i
})
}
data := errorTemplateData{
Err: template.HTML(msg),
// TODO: localize status codes? might be fine with a few
Status: http.StatusText(code),
StatusCode: code,
}
renderErr := eh.render.Render(rw, req, "error.tmpl", code, data)
if renderErr != nil {
logger := logging.FromContext(req.Context())
level.Error(logger).Log("event", "error template renderfailed",
"orig-err", err,
"render-err", renderErr,
)
}
}
type errorTemplateData struct {
StatusCode int
Status string
Err template.HTML
return code, msg
}

109
web/errors/flashes.go Normal file
View File

@ -0,0 +1,109 @@
package errors
import (
"encoding/gob"
"fmt"
"net/http"
"github.com/gorilla/sessions"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
)
type FlashHelper struct {
store sessions.Store
locHelper *i18n.Helper
}
func NewFlashHelper(s sessions.Store, loc *i18n.Helper) *FlashHelper {
gob.Register(FlashMessage{})
return &FlashHelper{
store: s,
locHelper: loc,
}
}
const flashSession = "go-ssb-room-flash-messages"
type FlashKind uint
const (
_ FlashKind = iota
// FlashError signals that a problem occured
FlashError
// FlashNotification represents a normal message (like "xyz added/updated successfull")
FlashNotification
)
type FlashMessage struct {
Kind FlashKind
Message string
}
// TODO: rethink error return - maybe panic() / maybe render package?
// AddMessage expects a i18n label, translates it and adds it as a FlashNotification
func (fh FlashHelper) AddMessage(rw http.ResponseWriter, req *http.Request, label string) {
session, err := fh.store.Get(req, flashSession)
if err != nil {
panic(fmt.Errorf("flashHelper: failed to get session: %w", err))
}
ih := fh.locHelper.FromRequest(req)
session.AddFlash(FlashMessage{
Kind: FlashNotification,
Message: ih.LocalizeSimple(label),
})
if err := session.Save(req, rw); err != nil {
panic(fmt.Errorf("flashHelper: failed to save session: %w", err))
}
}
// AddError adds a FlashError and translates the passed err using localizeError()
func (fh FlashHelper) AddError(rw http.ResponseWriter, req *http.Request, err error) {
session, getErr := fh.store.Get(req, flashSession)
if getErr != nil {
panic(fmt.Errorf("flashHelper: failed to get session: %w", err))
}
ih := fh.locHelper.FromRequest(req)
_, msg := localizeError(ih, err)
session.AddFlash(FlashMessage{
Kind: FlashError,
Message: msg,
})
if err := session.Save(req, rw); err != nil {
panic(fmt.Errorf("flashHelper: failed to save session: %w", err))
}
}
// GetAll returns all the FlashMessages, emptys and updates the store
func (fh FlashHelper) GetAll(rw http.ResponseWriter, req *http.Request) ([]FlashMessage, error) {
session, err := fh.store.Get(req, flashSession)
if err != nil {
return nil, err
}
opaqueFlashes := session.Flashes()
flashes := make([]FlashMessage, len(opaqueFlashes))
for i, of := range opaqueFlashes {
f, ok := of.(FlashMessage)
if !ok {
return nil, fmt.Errorf("GetFlashes: failed to unpack flash: %T", of)
}
flashes[i].Kind = f.Kind
flashes[i].Message = f.Message
}
err = session.Save(req, rw)
return flashes, err
}

View File

@ -3,7 +3,6 @@
package admin
import (
"errors"
"fmt"
"net/http"
"strconv"
@ -20,6 +19,8 @@ import (
type aliasesHandler struct {
r *render.Renderer
flashes *weberrors.FlashHelper
db roomdb.AliasesService
}
@ -38,11 +39,8 @@ func (h aliasesHandler) revokeConfirm(rw http.ResponseWriter, req *http.Request)
entry, err := h.db.GetByID(req.Context(), id)
if err != nil {
if errors.Is(err, roomdb.ErrNotFound) {
http.Redirect(rw, req, redirectToAliases, http.StatusFound)
return nil, ErrRedirected
}
return nil, err
h.flashes.AddError(rw, req, err)
return nil, weberrors.ErrRedirect{Path: redirectToAliases}
}
return map[string]interface{}{
@ -65,17 +63,12 @@ func (h aliasesHandler) revoke(rw http.ResponseWriter, req *http.Request) {
return
}
status := http.StatusFound
status := http.StatusTemporaryRedirect
err = h.db.Revoke(req.Context(), req.FormValue("name"))
if err != nil {
if !errors.Is(err, roomdb.ErrNotFound) {
// TODO: flash error
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
}
status = http.StatusNotFound
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
h.flashes.AddError(rw, req, err)
} else {
h.flashes.AddMessage(rw, req, "AdminAliasRevoked")
}
http.Redirect(rw, req, redirectToAliases, status)

View File

@ -8,7 +8,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
refs "go.mindeco.de/ssb-refs"
@ -23,10 +22,9 @@ func TestAliasesRevokeConfirmation(t *testing.T) {
testEntry := roomdb.Alias{ID: 666, Name: "the-test-name", Feed: *testKey}
ts.AliasesDB.GetByIDReturns(testEntry, nil)
urlTo := web.NewURLTo(ts.Router)
urlRevokeConfirm := urlTo(router.AdminAliasesRevokeConfirm, "id", 3)
urlRevokeConfirm := ts.URLTo(router.AdminAliasesRevokeConfirm, "id", 3)
html, resp := ts.Client.GetHTML(urlRevokeConfirm.String())
html, resp := ts.Client.GetHTML(urlRevokeConfirm)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
a.Equal(testKey.Ref(), html.Find("pre#verify").Text(), "has the key for verification")
@ -40,10 +38,8 @@ func TestAliasesRevokeConfirmation(t *testing.T) {
action, ok := form.Attr("action")
a.True(ok, "form has action set")
addURL, err := ts.Router.Get(router.AdminAliasesRevoke).URL()
a.NoError(err)
a.Equal(addURL.String(), action)
addURL := ts.URLTo(router.AdminAliasesRevoke)
a.Equal(addURL.Path, action)
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "name", Type: "hidden", Value: testEntry.Name},
@ -54,14 +50,23 @@ func TestAliasesRevoke(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
urlTo := web.NewURLTo(ts.Router)
urlRevoke := urlTo(router.AdminAliasesRevoke)
urlRevoke := ts.URLTo(router.AdminAliasesRevoke)
overviewURL := ts.URLTo(router.AdminAliasesOverview)
ts.AliasesDB.RevokeReturns(nil)
addVals := url.Values{"name": []string{"the-name"}}
rec := ts.Client.PostForm(urlRevoke.String(), addVals)
a.Equal(http.StatusFound, rec.Code)
rec := ts.Client.PostForm(urlRevoke, addVals)
a.Equal(http.StatusTemporaryRedirect, rec.Code)
a.Equal(overviewURL.Path, rec.Header().Get("Location"))
a.True(len(rec.Result().Cookies()) > 0, "got a cookie")
// check flash messages
doc, resp := ts.Client.GetHTML(overviewURL)
a.Equal(http.StatusOK, resp.Code)
flashes := doc.Find("#flashes-list").Children()
a.Equal(1, flashes.Length())
a.Equal("AdminAliasRevoked", flashes.Text())
a.Equal(1, ts.AliasesDB.RevokeCallCount())
_, theName := ts.AliasesDB.RevokeArgsForCall(0)
@ -70,7 +75,15 @@ func TestAliasesRevoke(t *testing.T) {
// now for unknown ID
ts.AliasesDB.RevokeReturns(roomdb.ErrNotFound)
addVals = url.Values{"name": []string{"nope"}}
rec = ts.Client.PostForm(urlRevoke.String(), addVals)
a.Equal(http.StatusNotFound, rec.Code)
//TODO: update redirect code with flash errors
rec = ts.Client.PostForm(urlRevoke, addVals)
a.Equal(http.StatusTemporaryRedirect, rec.Code)
a.Equal(overviewURL.Path, rec.Header().Get("Location"))
a.True(len(rec.Result().Cookies()) > 0, "got a cookie")
// check flash messages
doc, resp = ts.Client.GetHTML(overviewURL)
a.Equal(http.StatusOK, resp.Code)
flashes = doc.Find("#flashes-list").Children()
a.Equal(1, flashes.Length())
a.Equal("ErrorNotFound", flashes.Text())
}

View File

@ -19,6 +19,8 @@ import (
type deniedKeysHandler struct {
r *render.Renderer
flashes *weberrors.FlashHelper
db roomdb.DeniedKeysService
}
@ -40,7 +42,8 @@ func (h deniedKeysHandler) add(w http.ResponseWriter, req *http.Request) {
newEntryParsed, err := refs.ParseFeedRef(newEntry)
if err != nil {
err = weberrors.ErrBadRequest{Where: "Public Key", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
h.flashes.AddError(w, req, err)
http.Redirect(w, req, redirectToDeniedKeys, http.StatusTemporaryRedirect)
return
}
@ -49,16 +52,12 @@ func (h deniedKeysHandler) add(w http.ResponseWriter, req *http.Request) {
err = h.db.Add(req.Context(), *newEntryParsed, comment)
if err != nil {
code := http.StatusInternalServerError
var aa roomdb.ErrAlreadyAdded
if errors.As(err, &aa) {
code = http.StatusBadRequest
}
h.r.Error(w, req, code, err)
return
h.flashes.AddError(w, req, err)
} else {
h.flashes.AddMessage(w, req, "AdminDeniedKeysAdded")
}
http.Redirect(w, req, redirectToDeniedKeys, http.StatusFound)
http.Redirect(w, req, redirectToDeniedKeys, http.StatusTemporaryRedirect)
}
func (h deniedKeysHandler) overview(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
@ -77,13 +76,14 @@ func (h deniedKeysHandler) overview(rw http.ResponseWriter, req *http.Request) (
}
pageData[csrf.TemplateTag] = csrf.TemplateField(req)
pageData["Flashes"], err = h.flashes.GetAll(rw, req)
if err != nil {
return nil, err
}
return pageData, nil
}
// TODO: move to render package so that we can decide to not render a page during the controller
var ErrRedirected = errors.New("render: not rendered but redirected")
func (h deniedKeysHandler) removeConfirm(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
id, err := strconv.ParseInt(req.URL.Query().Get("id"), 10, 64)
if err != nil {
@ -93,12 +93,8 @@ func (h deniedKeysHandler) removeConfirm(rw http.ResponseWriter, req *http.Reque
entry, err := h.db.GetByID(req.Context(), id)
if err != nil {
if errors.Is(err, roomdb.ErrNotFound) {
// TODO "flash" errors
http.Redirect(rw, req, redirectToDeniedKeys, http.StatusFound)
return nil, ErrRedirected
}
return nil, err
h.flashes.AddError(rw, req, err)
return nil, weberrors.ErrRedirect{Path: redirectToDeniedKeys}
}
return map[string]interface{}{
@ -112,7 +108,7 @@ func (h deniedKeysHandler) remove(rw http.ResponseWriter, req *http.Request) {
if err != nil {
err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
// TODO "flash" errors
http.Redirect(rw, req, redirectToDeniedKeys, http.StatusFound)
http.Redirect(rw, req, redirectToDeniedKeys, http.StatusTemporaryRedirect)
return
}
@ -120,7 +116,7 @@ func (h deniedKeysHandler) remove(rw http.ResponseWriter, req *http.Request) {
if err != nil {
err = weberrors.ErrBadRequest{Where: "ID", Details: err}
// TODO "flash" errors
http.Redirect(rw, req, redirectToDeniedKeys, http.StatusFound)
http.Redirect(rw, req, redirectToDeniedKeys, http.StatusTemporaryRedirect)
return
}

View File

@ -6,15 +6,11 @@ import (
"bytes"
"net/http"
"net/url"
"strings"
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
refs "go.mindeco.de/ssb-refs"
@ -24,10 +20,9 @@ func TestDeniedKeysEmpty(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
url, err := ts.Router.Get(router.AdminDeniedKeysOverview).URL()
a.Nil(err)
url := ts.URLTo(router.AdminDeniedKeysOverview)
html, resp := ts.Client.GetHTML(url.String())
html, resp := ts.Client.GetHTML(url)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
@ -41,10 +36,9 @@ func TestDeniedKeysAdd(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
listURL, err := ts.Router.Get(router.AdminDeniedKeysOverview).URL()
a.NoError(err)
listURL := ts.URLTo(router.AdminDeniedKeysOverview)
html, resp := ts.Client.GetHTML(listURL.String())
html, resp := ts.Client.GetHTML(listURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
formSelection := html.Find("form#add-entry")
@ -57,10 +51,8 @@ func TestDeniedKeysAdd(t *testing.T) {
action, ok := formSelection.Attr("action")
a.True(ok, "form has action set")
addURL, err := ts.Router.Get(router.AdminDeniedKeysAdd).URL()
a.NoError(err)
a.Equal(addURL.String(), action)
addURL := ts.URLTo(router.AdminDeniedKeysAdd)
a.Equal(addURL.Path, action)
webassert.ElementsInForm(t, formSelection, []webassert.FormElement{
{Name: "pub_key", Type: "text"},
@ -73,8 +65,8 @@ func TestDeniedKeysAdd(t *testing.T) {
// just any key that looks valid
"pub_key": []string{newKey},
}
rec := ts.Client.PostForm(addURL.String(), addVals)
a.Equal(http.StatusFound, rec.Code)
rec := ts.Client.PostForm(addURL, addVals)
a.Equal(http.StatusTemporaryRedirect, rec.Code)
a.Equal(1, ts.DeniedKeysDB.AddCallCount())
_, addedKey, addedComment := ts.DeniedKeysDB.AddArgsForCall(0)
@ -85,29 +77,30 @@ func TestDeniedKeysAdd(t *testing.T) {
func TestDeniedKeysDontAddInvalid(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
r := require.New(t)
addURL, err := ts.Router.Get(router.AdminDeniedKeysAdd).URL()
a.NoError(err)
addURL := ts.URLTo(router.AdminDeniedKeysAdd)
newKey := "@some-garbage"
addVals := url.Values{
"comment": []string{"some-comment"},
"pub_key": []string{newKey},
}
rec := ts.Client.PostForm(addURL.String(), addVals)
a.Equal(http.StatusBadRequest, rec.Code)
rec := ts.Client.PostForm(addURL, addVals)
a.Equal(http.StatusTemporaryRedirect, rec.Code)
a.Equal(0, ts.DeniedKeysDB.AddCallCount())
a.Equal(0, ts.DeniedKeysDB.AddCallCount(), "did not call add")
doc, err := goquery.NewDocumentFromReader(rec.Body)
r.NoError(err)
listURL := ts.URLTo(router.AdminDeniedKeysOverview)
res := rec.Result()
a.Equal(listURL.Path, res.Header.Get("Location"), "redirecting to overview")
a.True(len(res.Cookies()) > 0, "got a cookie (flash msg)")
expErr := `bad request: feedRef: couldn't parse "@some-garbage"`
gotMsg := doc.Find("#errBody").Text()
if !a.True(strings.HasPrefix(gotMsg, expErr), "did not find errBody") {
t.Log(gotMsg)
}
doc, resp := ts.Client.GetHTML(listURL)
a.Equal(http.StatusOK, resp.Code)
flashes := doc.Find("#flashes-list").Children()
a.Equal(1, flashes.Length())
a.Equal("ErrorBadRequest", flashes.Text())
}
func TestDeniedKeys(t *testing.T) {
@ -121,7 +114,9 @@ func TestDeniedKeys(t *testing.T) {
}
ts.DeniedKeysDB.ListReturns(lst, nil)
html, resp := ts.Client.GetHTML("/denied")
deniedOverviewURL := ts.URLTo(router.AdminDeniedKeysOverview)
html, resp := ts.Client.GetHTML(deniedOverviewURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
@ -137,7 +132,7 @@ func TestDeniedKeys(t *testing.T) {
}
ts.DeniedKeysDB.ListReturns(lst, nil)
html, resp = ts.Client.GetHTML("/denied")
html, resp = ts.Client.GetHTML(deniedOverviewURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
@ -164,10 +159,9 @@ func TestDeniedKeysRemoveConfirmation(t *testing.T) {
testEntry := roomdb.ListEntry{ID: 666, PubKey: *testKey}
ts.DeniedKeysDB.GetByIDReturns(testEntry, nil)
urlTo := web.NewURLTo(ts.Router)
urlRemoveConfirm := urlTo(router.AdminDeniedKeysRemoveConfirm, "id", 3)
urlRemoveConfirm := ts.URLTo(router.AdminDeniedKeysRemoveConfirm, "id", 3)
html, resp := ts.Client.GetHTML(urlRemoveConfirm.String())
html, resp := ts.Client.GetHTML(urlRemoveConfirm)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
a.Equal(testKey.Ref(), html.Find("pre#verify").Text(), "has the key for verification")
@ -181,10 +175,8 @@ func TestDeniedKeysRemoveConfirmation(t *testing.T) {
action, ok := form.Attr("action")
a.True(ok, "form has action set")
addURL, err := ts.Router.Get(router.AdminDeniedKeysRemove).URL()
a.NoError(err)
a.Equal(addURL.String(), action)
addURL := ts.URLTo(router.AdminDeniedKeysRemove)
a.Equal(addURL.Path, action)
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "id", Type: "hidden", Value: "666"},
@ -195,13 +187,12 @@ func TestDeniedKeysRemove(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
urlTo := web.NewURLTo(ts.Router)
urlRemove := urlTo(router.AdminDeniedKeysRemove)
urlRemove := ts.URLTo(router.AdminDeniedKeysRemove)
ts.DeniedKeysDB.RemoveIDReturns(nil)
addVals := url.Values{"id": []string{"666"}}
rec := ts.Client.PostForm(urlRemove.String(), addVals)
rec := ts.Client.PostForm(urlRemove, addVals)
a.Equal(http.StatusFound, rec.Code)
a.Equal(1, ts.DeniedKeysDB.RemoveIDCallCount())
@ -211,7 +202,7 @@ func TestDeniedKeysRemove(t *testing.T) {
// now for unknown ID
ts.DeniedKeysDB.RemoveIDReturns(roomdb.ErrNotFound)
addVals = url.Values{"id": []string{"667"}}
rec = ts.Client.PostForm(urlRemove.String(), addVals)
rec = ts.Client.PostForm(urlRemove, addVals)
a.Equal(http.StatusNotFound, rec.Code)
//TODO: update redirect code with flash errors
}

View File

@ -18,6 +18,7 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
)
// HTMLTemplates define the list of files the template system should load.
@ -60,11 +61,11 @@ func Handler(
domainName string,
r *render.Renderer,
roomState *roomstate.Manager,
fh *weberrors.FlashHelper,
dbs Databases,
) http.Handler {
mux := &http.ServeMux{}
// TODO: configure 404 handler
var dashboardHandler = dashboardHandler{
r: r,
dbs: dbs,
@ -84,14 +85,18 @@ func Handler(
}))
var ah = aliasesHandler{
r: r,
r: r,
flashes: fh,
db: dbs.Aliases,
}
mux.HandleFunc("/aliases/revoke/confirm", r.HTML("admin/aliases-revoke-confirm.tmpl", ah.revokeConfirm))
mux.HandleFunc("/aliases/revoke", ah.revoke)
var dh = deniedKeysHandler{
r: r,
r: r,
flashes: fh,
db: dbs.DeniedKeys,
}
mux.HandleFunc("/denied", r.HTML("admin/denied-keys.tmpl", dh.overview))
@ -100,7 +105,9 @@ func Handler(
mux.HandleFunc("/denied/remove", dh.remove)
var mh = membersHandler{
r: r,
r: r,
flashes: fh,
db: dbs.Members,
}
mux.HandleFunc("/member", r.HTML("admin/member.tmpl", mh.details))
@ -111,7 +118,9 @@ func Handler(
mux.HandleFunc("/members/remove", mh.remove)
var ih = invitesHandler{
r: r,
r: r,
flashes: fh,
db: dbs.Invites,
config: dbs.Config,
@ -124,7 +133,9 @@ func Handler(
mux.HandleFunc("/invites/revoke", ih.revoke)
var nh = noticeHandler{
r: r,
r: r,
flashes: fh,
noticeDB: dbs.Notices,
pinnedDB: dbs.PinnedNotices,
}
@ -133,6 +144,11 @@ func Handler(
mux.HandleFunc("/notice/translation/add", nh.addTranslation)
mux.HandleFunc("/notice/save", nh.save)
// path:/ matches everything that isn't registerd (ie. its the "Not Found handler")
mux.HandleFunc("/", http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
r.Error(rw, req, 404, weberrors.ErrNotFound{What: req.URL.Path})
}))
return customStripPrefix("/admin", mux)
}

View File

@ -21,10 +21,9 @@ func TestDashoard(t *testing.T) {
ts.InvitesDB.CountReturns(3, nil) // 3 invites
ts.DeniedKeysDB.CountReturns(2, nil) // 2 banned
url, err := ts.Router.Get(router.AdminDashboard).URL()
a.Nil(err)
dashURL := ts.URLTo(router.AdminDashboard)
html, resp := ts.Client.GetHTML(url.String())
html, resp := ts.Client.GetHTML(dashURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
a.Equal("1", html.Find("#online-count").Text())

View File

@ -17,7 +17,8 @@ import (
)
type invitesHandler struct {
r *render.Renderer
r *render.Renderer
flashes *weberrors.FlashHelper
db roomdb.InvitesService
config roomdb.RoomConfig
@ -42,6 +43,10 @@ func (h invitesHandler) overview(rw http.ResponseWriter, req *http.Request) (int
}
pageData[csrf.TemplateTag] = csrf.TemplateField(req)
pageData["Flashes"], err = h.flashes.GetAll(rw, req)
if err != nil {
return nil, err
}
return pageData, nil
}

View File

@ -10,7 +10,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
)
@ -28,7 +27,9 @@ func TestInvitesOverview(t *testing.T) {
}
ts.InvitesDB.ListReturns(lst, nil)
html, resp := ts.Client.GetHTML("/invites")
invitesOverviewURL := ts.URLTo(router.AdminInvitesOverview)
html, resp := ts.Client.GetHTML(invitesOverviewURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
@ -46,7 +47,7 @@ func TestInvitesOverview(t *testing.T) {
}
ts.InvitesDB.ListReturns(lst, nil)
html, resp = ts.Client.GetHTML("/invites")
html, resp = ts.Client.GetHTML(invitesOverviewURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
@ -68,10 +69,9 @@ func TestInvitesCreateForm(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
url, err := ts.Router.Get(router.AdminInvitesOverview).URL()
a.Nil(err)
overviewURL := ts.URLTo(router.AdminInvitesOverview)
html, resp := ts.Client.GetHTML(url.String())
html, resp := ts.Client.GetHTML(overviewURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
@ -89,10 +89,8 @@ func TestInvitesCreateForm(t *testing.T) {
action, ok := formSelection.Attr("action")
a.True(ok, "form has action set")
addURL, err := ts.Router.Get(router.AdminInvitesCreate).URL()
a.NoError(err)
a.Equal(addURL.String(), action)
addURL := ts.URLTo(router.AdminInvitesCreate)
a.Equal(addURL.Path, action)
}
func TestInvitesCreate(t *testing.T) {
@ -100,13 +98,12 @@ func TestInvitesCreate(t *testing.T) {
a := assert.New(t)
r := require.New(t)
urlTo := web.NewURLTo(ts.Router)
urlRemove := urlTo(router.AdminInvitesCreate)
urlRemove := ts.URLTo(router.AdminInvitesCreate)
testInvite := "your-fake-test-invite"
ts.InvitesDB.CreateReturns(testInvite, nil)
rec := ts.Client.PostForm(urlRemove.String(), url.Values{})
rec := ts.Client.PostForm(urlRemove, url.Values{})
a.Equal(http.StatusOK, rec.Code)
r.Equal(1, ts.InvitesDB.CreateCallCount(), "expected one invites.Create call")
@ -121,9 +118,7 @@ func TestInvitesCreate(t *testing.T) {
{"#welcome", "AdminInviteCreatedTitle" + "AdminInviteCreatedInstruct"},
})
wantURL := urlTo(router.CompleteInviteFacade, "token", testInvite)
wantURL.Host = ts.Domain
wantURL.Scheme = "https"
wantURL := ts.URLTo(router.CompleteInviteFacade, "token", testInvite)
shownLink := doc.Find("#invite-facade-link").Text()
a.Equal(wantURL.String(), shownLink)

View File

@ -22,6 +22,8 @@ import (
type membersHandler struct {
r *render.Renderer
flashes *weberrors.FlashHelper
db roomdb.MembersService
}
@ -44,22 +46,19 @@ func (h membersHandler) add(w http.ResponseWriter, req *http.Request) {
newEntryParsed, err := refs.ParseFeedRef(newEntry)
if err != nil {
err = weberrors.ErrBadRequest{Where: "Public Key", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
h.flashes.AddError(w, req, err)
http.Redirect(w, req, redirectToMembers, http.StatusTemporaryRedirect)
return
}
_, err = h.db.Add(req.Context(), *newEntryParsed, roomdb.RoleMember)
if err != nil {
code := http.StatusInternalServerError
var aa roomdb.ErrAlreadyAdded
if errors.As(err, &aa) {
code = http.StatusBadRequest
}
h.r.Error(w, req, code, err)
return
h.flashes.AddError(w, req, err)
} else {
h.flashes.AddMessage(w, req, "AdminMemberAdded")
}
http.Redirect(w, req, redirectToMembers, http.StatusFound)
http.Redirect(w, req, redirectToMembers, http.StatusTemporaryRedirect)
}
func (h membersHandler) changeRole(w http.ResponseWriter, req *http.Request) {
@ -98,11 +97,12 @@ func (h membersHandler) changeRole(w http.ResponseWriter, req *http.Request) {
if err := h.db.SetRole(req.Context(), memberID, role); err != nil {
err = weberrors.DatabaseError{Reason: err}
// TODO: not found error
h.r.Error(w, req, http.StatusInternalServerError, err)
return
}
h.flashes.AddMessage(w, req, "AdminMemberUpdated")
urlTo := web.NewURLTo(router.CompleteApp())
memberDetailsURL := urlTo(router.AdminMemberDetails, "id", memberID).String()
http.Redirect(w, req, memberDetailsURL, http.StatusTemporaryRedirect)
@ -127,6 +127,11 @@ func (h membersHandler) overview(rw http.ResponseWriter, req *http.Request) (int
pageData["AllRoles"] = []roomdb.Role{roomdb.RoleMember, roomdb.RoleModerator, roomdb.RoleAdmin}
pageData["Flashes"], err = h.flashes.GetAll(rw, req)
if err != nil {
return nil, err
}
return pageData, nil
}
@ -162,8 +167,8 @@ func (h membersHandler) removeConfirm(rw http.ResponseWriter, req *http.Request)
entry, err := h.db.GetByID(req.Context(), id)
if err != nil {
if errors.Is(err, roomdb.ErrNotFound) {
http.Redirect(rw, req, redirectToMembers, http.StatusFound)
return nil, ErrRedirected
h.flashes.AddError(rw, req, err)
return nil, weberrors.ErrRedirect{Path: redirectToMembers}
}
return nil, err
}
@ -175,32 +180,32 @@ func (h membersHandler) removeConfirm(rw http.ResponseWriter, req *http.Request)
}
func (h membersHandler) remove(rw http.ResponseWriter, req *http.Request) {
err := req.ParseForm()
if err != nil {
if req.Method != "POST" {
err := weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST not %s", req.Method)}
h.r.Error(rw, req, http.StatusBadRequest, err)
return
}
if err := req.ParseForm(); err != nil {
err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
// TODO "flash" errors
http.Redirect(rw, req, redirectToMembers, http.StatusFound)
h.r.Error(rw, req, http.StatusBadRequest, err)
return
}
id, err := strconv.ParseInt(req.FormValue("id"), 10, 64)
if err != nil {
err = weberrors.ErrBadRequest{Where: "ID", Details: err}
// TODO "flash" errors
http.Redirect(rw, req, redirectToMembers, http.StatusFound)
h.flashes.AddError(rw, req, err)
http.Redirect(rw, req, redirectToMembers, http.StatusTemporaryRedirect)
return
}
status := http.StatusFound
err = h.db.RemoveID(req.Context(), id)
if err != nil {
if !errors.Is(err, roomdb.ErrNotFound) {
// TODO "flash" errors
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
}
status = http.StatusNotFound
h.flashes.AddError(rw, req, err)
} else {
h.flashes.AddMessage(rw, req, "AdminMemberRemoved")
}
http.Redirect(rw, req, redirectToMembers, status)
http.Redirect(rw, req, redirectToMembers, http.StatusTemporaryRedirect)
}

View File

@ -4,16 +4,11 @@ import (
"bytes"
"net/http"
"net/url"
"strings"
"testing"
"github.com/PuerkitoBio/goquery"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
refs "go.mindeco.de/ssb-refs"
@ -23,10 +18,9 @@ func TestMembersEmpty(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
url, err := ts.Router.Get(router.AdminMembersOverview).URL()
a.Nil(err)
url := ts.URLTo(router.AdminMembersOverview)
html, resp := ts.Client.GetHTML(url.String())
html, resp := ts.Client.GetHTML(url)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
@ -40,10 +34,9 @@ func TestMembersAdd(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
listURL, err := ts.Router.Get(router.AdminMembersOverview).URL()
a.NoError(err)
listURL := ts.URLTo(router.AdminMembersOverview)
html, resp := ts.Client.GetHTML(listURL.String())
html, resp := ts.Client.GetHTML(listURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
formSelection := html.Find("form#add-entry")
@ -56,10 +49,8 @@ func TestMembersAdd(t *testing.T) {
action, ok := formSelection.Attr("action")
a.True(ok, "form has action set")
addURL, err := ts.Router.Get(router.AdminMembersAdd).URL()
a.NoError(err)
a.Equal(addURL.String(), action)
addURL := ts.URLTo(router.AdminMembersAdd)
a.Equal(addURL.Path, action)
webassert.ElementsInForm(t, formSelection, []webassert.FormElement{
{Name: "pub_key", Type: "text"},
@ -70,8 +61,8 @@ func TestMembersAdd(t *testing.T) {
// just any key that looks valid
"pub_key": []string{newKey},
}
rec := ts.Client.PostForm(addURL.String(), addVals)
a.Equal(http.StatusFound, rec.Code)
rec := ts.Client.PostForm(addURL, addVals)
a.Equal(http.StatusTemporaryRedirect, rec.Code)
a.Equal(1, ts.MembersDB.AddCallCount())
_, addedPubKey, addedRole := ts.MembersDB.AddArgsForCall(0)
@ -83,29 +74,31 @@ func TestMembersAdd(t *testing.T) {
func TestMembersDontAddInvalid(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
r := require.New(t)
addURL, err := ts.Router.Get(router.AdminMembersAdd).URL()
a.NoError(err)
addURL := ts.URLTo(router.AdminMembersAdd)
newKey := "@some-garbage"
addVals := url.Values{
"nick": []string{"some-test-nick"},
"pub_key": []string{newKey},
}
rec := ts.Client.PostForm(addURL.String(), addVals)
a.Equal(http.StatusBadRequest, rec.Code)
rec := ts.Client.PostForm(addURL, addVals)
a.Equal(http.StatusTemporaryRedirect, rec.Code)
a.Equal(0, ts.MembersDB.AddCallCount())
doc, err := goquery.NewDocumentFromReader(rec.Body)
r.NoError(err)
listURL := ts.URLTo(router.AdminMembersOverview)
res := rec.Result()
a.Equal(listURL.Path, res.Header.Get("Location"), "redirecting to overview")
a.True(len(res.Cookies()) > 0, "got a cookie (flash msg)")
doc, resp := ts.Client.GetHTML(listURL)
a.Equal(http.StatusOK, resp.Code)
flashes := doc.Find("#flashes-list").Children()
a.Equal(1, flashes.Length())
a.Equal("ErrorBadRequest", flashes.Text())
expErr := `bad public key: feedRef: couldn't parse "@some-garbage"`
gotMsg := doc.Find("#errBody").Text()
if !a.True(strings.HasPrefix(gotMsg, expErr), "did not find errBody") {
t.Log(gotMsg)
}
}
func TestMembers(t *testing.T) {
@ -119,7 +112,9 @@ func TestMembers(t *testing.T) {
}
ts.MembersDB.ListReturns(lst, nil)
html, resp := ts.Client.GetHTML("/members")
membersOveriwURL := ts.URLTo(router.AdminMembersOverview)
html, resp := ts.Client.GetHTML(membersOveriwURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
@ -135,7 +130,7 @@ func TestMembers(t *testing.T) {
}
ts.MembersDB.ListReturns(lst, nil)
html, resp = ts.Client.GetHTML("/members")
html, resp = ts.Client.GetHTML(membersOveriwURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
@ -218,10 +213,9 @@ func TestMembersRemoveConfirmation(t *testing.T) {
testEntry := roomdb.Member{ID: 666, PubKey: *testKey}
ts.MembersDB.GetByIDReturns(testEntry, nil)
urlTo := web.NewURLTo(ts.Router)
urlRemoveConfirm := urlTo(router.AdminMembersRemoveConfirm, "id", 3)
urlRemoveConfirm := ts.URLTo(router.AdminMembersRemoveConfirm, "id", 3)
html, resp := ts.Client.GetHTML(urlRemoveConfirm.String())
html, resp := ts.Client.GetHTML(urlRemoveConfirm)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
a.Equal(testKey.Ref(), html.Find("pre#verify").Text(), "has the key for verification")
@ -235,10 +229,8 @@ func TestMembersRemoveConfirmation(t *testing.T) {
action, ok := form.Attr("action")
a.True(ok, "form has action set")
addURL, err := ts.Router.Get(router.AdminMembersRemove).URL()
a.NoError(err)
a.Equal(addURL.String(), action)
addURL := ts.URLTo(router.AdminMembersRemove)
a.Equal(addURL.Path, action)
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "id", Type: "hidden", Value: "666"},
@ -249,23 +241,46 @@ func TestMembersRemove(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
urlTo := web.NewURLTo(ts.Router)
urlRemove := urlTo(router.AdminMembersRemove)
urlRemove := ts.URLTo(router.AdminMembersRemove)
ts.MembersDB.RemoveIDReturns(nil)
addVals := url.Values{"id": []string{"666"}}
rec := ts.Client.PostForm(urlRemove.String(), addVals)
a.Equal(http.StatusFound, rec.Code)
rec := ts.Client.PostForm(urlRemove, addVals)
a.Equal(http.StatusTemporaryRedirect, rec.Code)
a.Equal(1, ts.MembersDB.RemoveIDCallCount())
_, theID := ts.MembersDB.RemoveIDArgsForCall(0)
a.EqualValues(666, theID)
listURL := ts.URLTo(router.AdminMembersOverview)
// check flash message
res := rec.Result()
a.Equal(listURL.Path, res.Header.Get("Location"), "redirecting to overview")
a.True(len(res.Cookies()) > 0, "got a cookie (flash msg)")
doc, resp := ts.Client.GetHTML(listURL)
a.Equal(http.StatusOK, resp.Code)
flashes := doc.Find("#flashes-list").Children()
a.Equal(1, flashes.Length())
a.Equal("AdminMemberRemoved", flashes.Text())
// now for unknown ID
ts.MembersDB.RemoveIDReturns(roomdb.ErrNotFound)
addVals = url.Values{"id": []string{"667"}}
rec = ts.Client.PostForm(urlRemove.String(), addVals)
a.Equal(http.StatusNotFound, rec.Code)
//TODO: update redirect code with flash errors
rec = ts.Client.PostForm(urlRemove, addVals)
a.Equal(http.StatusTemporaryRedirect, rec.Code)
// check flash message
res = rec.Result()
a.Equal(listURL.Path, res.Header.Get("Location"), "redirecting to overview")
a.True(len(res.Cookies()) > 0, "got a cookie (flash msg)")
doc, resp = ts.Client.GetHTML(listURL)
a.Equal(http.StatusOK, resp.Code)
flashes = doc.Find("#flashes-list").Children()
a.Equal(1, flashes.Length())
a.Equal("ErrorNotFound", flashes.Text())
}

View File

@ -19,6 +19,8 @@ import (
type noticeHandler struct {
r *render.Renderer
flashes *weberrors.FlashHelper
noticeDB roomdb.NoticesService
pinnedDB roomdb.PinnedNoticesService
}

View File

@ -7,7 +7,6 @@ import (
"testing"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
"github.com/stretchr/testify/assert"
@ -17,8 +16,6 @@ import (
func TestNoticeSaveActuallyCalled(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
// instantiate the urlTo helper (constructs urls for us!)
urlTo := web.NewURLTo(ts.Router)
id := []string{"1"}
title := []string{"SSB Breaking News: This Test Is Great"}
@ -26,9 +23,9 @@ func TestNoticeSaveActuallyCalled(t *testing.T) {
language := []string{"en-GB"}
// POST a correct request to the save handler, and verify that the save was handled using the mock database)
u := urlTo(router.AdminNoticeSave)
u := ts.URLTo(router.AdminNoticeSave)
formValues := url.Values{"id": id, "title": title, "content": content, "language": language}
resp := ts.Client.PostForm(u.String(), formValues)
resp := ts.Client.PostForm(u, formValues)
a.Equal(http.StatusSeeOther, resp.Code, "POST should work")
a.Equal(1, ts.NoticeDB.SaveCallCount(), "noticedb should have saved after POST completed")
}
@ -37,8 +34,6 @@ func TestNoticeSaveActuallyCalled(t *testing.T) {
func TestNoticeSaveRefusesIncomplete(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
// instantiate the urlTo helper (constructs urls for us!)
urlTo := web.NewURLTo(ts.Router)
// notice values we are selectively omitting in the tests below
id := []string{"1"}
@ -47,24 +42,24 @@ func TestNoticeSaveRefusesIncomplete(t *testing.T) {
language := []string{"pt"}
/* save without id */
u := urlTo(router.AdminNoticeSave)
u := ts.URLTo(router.AdminNoticeSave)
emptyParams := url.Values{}
resp := ts.Client.PostForm(u.String(), emptyParams)
resp := ts.Client.PostForm(u, emptyParams)
a.Equal(http.StatusInternalServerError, resp.Code, "saving without id should not work")
/* save without title */
formValues := url.Values{"id": id, "content": content, "language": language}
resp = ts.Client.PostForm(u.String(), formValues)
resp = ts.Client.PostForm(u, formValues)
a.Equal(http.StatusInternalServerError, resp.Code, "saving without title should not work")
/* save without content */
formValues = url.Values{"id": id, "title": title, "language": language}
resp = ts.Client.PostForm(u.String(), formValues)
resp = ts.Client.PostForm(u, formValues)
a.Equal(http.StatusInternalServerError, resp.Code, "saving without content should not work")
/* save without language */
formValues = url.Values{"id": id, "title": title, "content": content}
resp = ts.Client.PostForm(u.String(), formValues)
resp = ts.Client.PostForm(u, formValues)
a.Equal(http.StatusInternalServerError, resp.Code, "saving without language should not work")
a.Equal(0, ts.NoticeDB.SaveCallCount(), "noticedb should never save incomplete requests")
@ -74,12 +69,11 @@ func TestNoticeSaveRefusesIncomplete(t *testing.T) {
func TestNoticeAddLanguageOnlyAllowsPost(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
// instantiate the urlTo helper (constructs urls for us!)
urlTo := web.NewURLTo(ts.Router)
// instantiate the ts.URLTo helper (constructs urls for us!)
// verify that a GET request is no bueno
u := urlTo(router.AdminNoticeAddTranslation, "name", roomdb.NoticeNews.String())
_, resp := ts.Client.GetHTML(u.String())
u := ts.URLTo(router.AdminNoticeAddTranslation, "name", roomdb.NoticeNews.String())
_, resp := ts.Client.GetHTML(u)
a.Equal(http.StatusMethodNotAllowed, resp.Code, "GET should not be allowed for this route")
// next up, we verify that a correct POST request actually works:
@ -89,7 +83,7 @@ func TestNoticeAddLanguageOnlyAllowsPost(t *testing.T) {
language := []string{"pt"}
formValues := url.Values{"name": []string{roomdb.NoticeNews.String()}, "id": id, "title": title, "content": content, "language": language}
resp = ts.Client.PostForm(u.String(), formValues)
resp = ts.Client.PostForm(u, formValues)
a.Equal(http.StatusTemporaryRedirect, resp.Code)
}
@ -97,8 +91,7 @@ func TestNoticeAddLanguageOnlyAllowsPost(t *testing.T) {
func TestNoticeDraftLanguageIncludesAllFields(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
// instantiate the urlTo helper (constructs urls for us!)
urlTo := web.NewURLTo(ts.Router)
// instantiate the ts.URLTo helper (constructs urls for us!)
// to test translations we first need to add a notice to the notice mockdb
notice := roomdb.Notice{
@ -110,8 +103,8 @@ func TestNoticeDraftLanguageIncludesAllFields(t *testing.T) {
// make sure we return a notice when accessing pinned notices (which are the only notices with translations at writing (2021-03-11)
ts.PinnedDB.GetReturns(&notice, nil)
u := urlTo(router.AdminNoticeDraftTranslation, "name", roomdb.NoticeNews.String())
html, resp := ts.Client.GetHTML(u.String())
u := ts.URLTo(router.AdminNoticeDraftTranslation, "name", roomdb.NoticeNews.String())
html, resp := ts.Client.GetHTML(u)
form := html.Find("form")
a.Equal(http.StatusOK, resp.Code, "Wrong HTTP status code")
// FormElement defaults to input if tag omitted
@ -125,8 +118,7 @@ func TestNoticeDraftLanguageIncludesAllFields(t *testing.T) {
func TestNoticeEditFormIncludesAllFields(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
// instantiate the urlTo helper (constructs urls for us!)
urlTo := web.NewURLTo(ts.Router)
// instantiate the ts.URLTo helper (constructs urls for us!)
// Create mock notice data to operate on
notice := roomdb.Notice{
@ -137,8 +129,8 @@ func TestNoticeEditFormIncludesAllFields(t *testing.T) {
}
ts.NoticeDB.GetByIDReturns(notice, nil)
u := urlTo(router.AdminNoticeEdit, "id", 1)
html, resp := ts.Client.GetHTML(u.String())
u := ts.URLTo(router.AdminNoticeEdit, "id", 1)
html, resp := ts.Client.GetHTML(u)
form := html.Find("form")
a.Equal(http.StatusOK, resp.Code, "Wrong HTTP status code")

View File

@ -4,30 +4,41 @@ package admin
import (
"context"
"crypto/rand"
"net/http"
"net/url"
"os"
"path/filepath"
"testing"
"time"
"github.com/dustin/go-humanize"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/pkg/errors"
"go.mindeco.de/http/render"
"go.mindeco.de/http/tester"
"go.mindeco.de/logging/logtest"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/randutil"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
weberrs "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n/i18ntesting"
"github.com/ssb-ngi-pointer/go-ssb-room/web/members"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
)
type testSession struct {
Domain string
Mux *http.ServeMux
Client *tester.Tester
Router *mux.Router
URLTo web.URLMaker
AliasesDB *mockdb.FakeAliasesService
ConfigDB *mockdb.FakeRoomConfig
@ -39,8 +50,6 @@ type testSession struct {
User roomdb.Member
Domain string
RoomState *roomstate.Manager
}
@ -62,49 +71,78 @@ func newSession(t *testing.T) *testSession {
ctx := context.TODO()
ts.RoomState = roomstate.NewManager(ctx, log)
ts.Router = router.CompleteApp()
ts.Domain = randutil.String(10)
// instantiate the urlTo helper (constructs urls for us!)
// the cookiejar in our custom http/tester needs a non-empty domain and scheme
router := router.CompleteApp()
urlTo := web.NewURLTo(router)
ts.URLTo = func(name string, vals ...interface{}) *url.URL {
testURL := urlTo(name, vals...)
if testURL == nil {
t.Fatalf("no URL for %s", name)
}
testURL.Host = ts.Domain
testURL.Scheme = "https" // fake
return testURL
}
// fake user
ts.User = roomdb.Member{
ID: 1234,
Role: roomdb.RoleModerator,
}
testPath := filepath.Join("testrun", t.Name())
os.RemoveAll(testPath)
i18ntesting.WriteReplacement(t)
testRepo := repo.New(testPath)
locHelper, err := i18n.New(testRepo)
if err != nil {
t.Fatal(err)
}
authKey := make([]byte, 64)
rand.Read(authKey)
encKey := make([]byte, 32)
rand.Read(encKey)
sessionsPath := filepath.Join(testPath, "sessions")
os.MkdirAll(sessionsPath, 0700)
fsStore := sessions.NewFilesystemStore(sessionsPath, authKey, encKey)
flashHelper := weberrs.NewFlashHelper(fsStore, locHelper)
// setup rendering
// TODO: make testing utils and move these there
testFuncs := web.TemplateFuncs(ts.Router)
testFuncs["i18n"] = func(msgID string) string { return msgID }
testFuncs["i18npl"] = func(msgID string, count int, _ ...interface{}) string {
if count == 1 {
return msgID + "Singular"
}
return msgID + "Plural"
}
testFuncs := web.TemplateFuncs(router)
testFuncs["current_page_is"] = func(routeName string) bool { return true }
testFuncs["is_logged_in"] = func() *roomdb.Member { return &ts.User }
testFuncs["urlToNotice"] = func(name string) string { return "" }
testFuncs["relative_time"] = func(when time.Time) string { return humanize.Time(when) }
r, err := render.New(web.Templates,
renderOpts := []render.Option{
render.SetLogger(log),
render.BaseTemplates("base.tmpl", "menu.tmpl"),
render.BaseTemplates("base.tmpl", "menu.tmpl", "flashes.tmpl"),
render.AddTemplates(append(HTMLTemplates, "error.tmpl")...),
render.ErrorTemplate("error.tmpl"),
render.FuncMap(testFuncs),
)
}
renderOpts = append(renderOpts, locHelper.GetRenderFuncs()...)
r, err := render.New(web.Templates, renderOpts...)
if err != nil {
t.Fatal(errors.Wrap(err, "setup: render init failed"))
}
ts.Mux = http.NewServeMux()
handler := Handler(
ts.Domain,
r,
ts.RoomState,
flashHelper,
Databases{
Aliases: ts.AliasesDB,
Config: ts.ConfigDB,
@ -118,6 +156,7 @@ func newSession(t *testing.T) *testSession {
handler = members.MiddlewareForTests(ts.User)(handler)
ts.Mux = http.NewServeMux()
ts.Mux.Handle("/", handler)
ts.Client = tester.New(ts.Mux, t)

View File

@ -32,23 +32,30 @@ func TestAliasResolve(t *testing.T) {
}
ts.AliasesDB.ResolveReturns(testAlias, nil)
// to construct the /alias/{name} url we need to bypass urlTo
// (which builds ?alias=name)
routes := router.CompleteApp()
// default is HTML
htmlURL, err := ts.Router.Get(router.CompleteAliasResolve).URL("alias", testAlias.Name)
r.Nil(err)
htmlURL, err := routes.Get(router.CompleteAliasResolve).URL("alias", testAlias.Name)
r.NoError(err)
t.Log("resolving", htmlURL.String())
html, resp := ts.Client.GetHTML(htmlURL.String())
html, resp := ts.Client.GetHTML(htmlURL)
a.Equal(http.StatusOK, resp.Code)
a.Equal(testAlias.Name, html.Find("title").Text())
// default is HTML
jsonURL, err := ts.Router.Get(router.CompleteAliasResolve).URL("alias", testAlias.Name)
r.Nil(err)
// now as JSON
jsonURL, err := routes.Get(router.CompleteAliasResolve).URL("alias", testAlias.Name)
r.NoError(err)
q := jsonURL.Query()
q.Set("encoding", "json")
jsonURL.RawQuery = q.Encode()
t.Log("resolving", jsonURL.String())
resp = ts.Client.GetBody(jsonURL.String())
resp = ts.Client.GetBody(jsonURL)
a.Equal(http.StatusOK, resp.Code)
var ar aliasJSONResponse

View File

@ -6,6 +6,7 @@ import (
"context"
"encoding/base64"
"encoding/gob"
"errors"
"fmt"
"html/template"
"image/color"
@ -223,9 +224,8 @@ func (h WithSSBHandler) DecideMethod(w http.ResponseWriter, req *http.Request) {
_, err := h.membersdb.GetByFeed(req.Context(), *cid)
if err != nil {
if err == roomdb.ErrNotFound {
errMsg := fmt.Errorf("ssb http auth: client isn't a member: %w", err)
h.render.Error(w, req, http.StatusForbidden, errMsg)
if errors.Is(err, roomdb.ErrNotFound) {
h.render.Error(w, req, http.StatusForbidden, weberrors.ErrForbidden{Details: err})
return
}
h.render.Error(w, req, http.StatusInternalServerError, err)
@ -273,11 +273,11 @@ func (h WithSSBHandler) clientInitiated(w http.ResponseWriter, req *http.Request
// check that we have that member
member, err := h.membersdb.GetByFeed(req.Context(), client)
if err != nil {
errMsg := fmt.Errorf("ssb http auth: client isn't a member: %w", err)
if err == roomdb.ErrNotFound {
if errors.Is(err, roomdb.ErrNotFound) {
errMsg := fmt.Errorf("ssb http auth: client isn't a member: %w", err)
return weberrors.ErrForbidden{Details: errMsg}
}
return errMsg
return err
}
payload.ClientID = client

View File

@ -8,7 +8,6 @@ import (
"encoding/base64"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"testing"
@ -21,7 +20,6 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/internal/maybemod/keys"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/signinwithssb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
refs "go.mindeco.de/ssb-refs"
@ -29,30 +27,32 @@ import (
func TestRestricted(t *testing.T) {
ts := setup(t)
a := assert.New(t)
testURLs := []string{
"/admin/admin",
"/admin/admin/",
"/admin/",
"/admin/anything/",
}
for _, turl := range testURLs {
for _, tstr := range testURLs {
turl, err := url.Parse(tstr)
if err != nil {
t.Fatal(err)
}
html, resp := ts.Client.GetHTML(turl)
a.Equal(http.StatusUnauthorized, resp.Code, "wrong HTTP status code for %q", turl)
a.Equal(http.StatusForbidden, resp.Code, "wrong HTTP status code for %q", turl)
found := html.Find("h1").Text()
a.Equal("Error #401 - Unauthorized", found, "wrong error message code for %q", turl)
a.Equal("Error #403 - Forbidden", found, "wrong error message code for %q", turl)
}
}
func TestLoginForm(t *testing.T) {
ts := setup(t)
a := assert.New(t)
a, r := assert.New(t), require.New(t)
url := ts.URLTo(router.AuthFallbackLogin)
url, err := ts.Router.Get(router.AuthFallbackLogin).URL()
r.Nil(err)
html, resp := ts.Client.GetHTML(url.String())
html, resp := ts.Client.GetHTML(url)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
@ -63,24 +63,15 @@ func TestLoginForm(t *testing.T) {
func TestFallbackAuth(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
a := assert.New(t)
// very cheap "browser" client session
jar, err := cookiejar.New(nil)
r.NoError(err)
signInFormURL := ts.URLTo(router.AuthFallbackLogin)
signInFormURL, err := ts.Router.Get(router.AuthFallbackLogin).URL()
r.Nil(err)
signInFormURL.Host = "localhost"
signInFormURL.Scheme = "https"
doc, resp := ts.Client.GetHTML(signInFormURL.String())
doc, resp := ts.Client.GetHTML(signInFormURL)
a.Equal(http.StatusOK, resp.Code)
csrfCookie := resp.Result().Cookies()
a.Len(csrfCookie, 1, "should have one cookie for CSRF protection validation")
jar.SetCookies(signInFormURL, csrfCookie)
a.True(len(csrfCookie) > 0, "should have one cookie for CSRF protection validation")
passwordForm := doc.Find("#password-fallback")
webassert.CSRFTokenPresent(t, passwordForm)
@ -102,53 +93,21 @@ func TestFallbackAuth(t *testing.T) {
}
ts.AuthFallbackDB.CheckReturns(int64(23), nil)
signInURL, err := ts.Router.Get(router.AuthFallbackFinalize).URL()
r.Nil(err)
signInURL := ts.URLTo(router.AuthFallbackFinalize)
signInURL.Host = "localhost"
signInURL.Scheme = "https"
var csrfCookieHeader = http.Header(map[string][]string{})
var csrfCookieHeader = make(http.Header)
csrfCookieHeader.Set("Referer", "https://localhost")
cs := jar.Cookies(signInURL)
r.Len(cs, 1, "expecting one cookie for csrf")
theCookie := cs[0].String()
a.NotEqual("", theCookie, "should have a new cookie")
csrfCookieHeader.Set("Cookie", theCookie)
ts.Client.SetHeaders(csrfCookieHeader)
resp = ts.Client.PostForm(signInURL.String(), loginVals)
resp = ts.Client.PostForm(signInURL, loginVals)
a.Equal(http.StatusSeeOther, resp.Code, "wrong HTTP status code for sign in")
a.Equal(1, ts.AuthFallbackDB.CheckCallCount())
sessionCookie := resp.Result().Cookies()
jar.SetCookies(signInURL, sessionCookie)
// now request the protected dashboard page
dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL()
r.Nil(err)
dashboardURL.Host = "localhost"
dashboardURL.Scheme = "https"
dashboardURL := ts.URLTo(router.AdminDashboard)
var sessionHeader = http.Header(map[string][]string{})
cs = jar.Cookies(dashboardURL)
// TODO: why doesnt this return the csrf cookie?!
// r.Len(cs, 2, "expecting one cookie!")
for _, c := range cs {
theCookie := c.String()
a.NotEqual("", theCookie, "should have a new cookie")
sessionHeader.Add("Cookie", theCookie)
}
durl := dashboardURL.String()
t.Log(durl)
// update headers
ts.Client.ClearHeaders()
ts.Client.SetHeaders(sessionHeader)
html, resp := ts.Client.GetHTML(durl)
html, resp := ts.Client.GetHTML(dashboardURL)
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
t.Log(html.Find("body").Text())
}
@ -160,7 +119,7 @@ func TestFallbackAuth(t *testing.T) {
testRef := refs.FeedRef{Algo: "test", ID: bytes.Repeat([]byte{0}, 16)}
ts.RoomState.AddEndpoint(testRef, nil)
html, resp = ts.Client.GetHTML(durl)
html, resp = ts.Client.GetHTML(dashboardURL)
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code") {
t.Log(html.Find("body").Text())
}
@ -171,7 +130,7 @@ func TestFallbackAuth(t *testing.T) {
testRef2 := refs.FeedRef{Algo: "test", ID: bytes.Repeat([]byte{1}, 16)}
ts.RoomState.AddEndpoint(testRef2, nil)
html, resp = ts.Client.GetHTML(durl)
html, resp = ts.Client.GetHTML(dashboardURL)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
@ -192,17 +151,13 @@ func TestAuthWithSSBClientInitNotConnected(t *testing.T) {
cc := signinwithssb.GenerateChallenge()
urlTo := web.NewURLTo(ts.Router)
signInStartURL := urlTo(router.AuthWithSSBLogin,
signInStartURL := ts.URLTo(router.AuthWithSSBLogin,
"cid", client.Feed.Ref(),
"cc", cc,
)
r.NotNil(signInStartURL)
t.Log(signInStartURL.String())
doc, resp := ts.Client.GetHTML(signInStartURL.String())
a.Equal(http.StatusInternalServerError, resp.Code) // TODO: StatusForbidden
doc, resp := ts.Client.GetHTML(signInStartURL)
a.Equal(http.StatusForbidden, resp.Code)
webassert.Localized(t, doc, []webassert.LocalizedElement{
// {"#welcome", "AuthWithSSBWelcome"},
@ -223,17 +178,15 @@ func TestAuthWithSSBClientInitNotAllowed(t *testing.T) {
cc := signinwithssb.GenerateChallenge()
urlTo := web.NewURLTo(ts.Router)
signInStartURL := urlTo(router.AuthWithSSBLogin,
signInStartURL := ts.URLTo(router.AuthWithSSBLogin,
"cid", client.Feed.Ref(),
"cc", cc,
)
r.NotNil(signInStartURL)
t.Log(signInStartURL.String())
doc, resp := ts.Client.GetHTML(signInStartURL.String())
doc, resp := ts.Client.GetHTML(signInStartURL)
a.Equal(http.StatusForbidden, resp.Code)
t.Log(resp.Body.String())
webassert.Localized(t, doc, []webassert.LocalizedElement{
// {"#welcome", "AuthWithSSBWelcome"},
@ -278,10 +231,6 @@ func TestAuthWithSSBClientInitHasClient(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
// very cheap "browser" client session
jar, err := cookiejar.New(nil)
r.NoError(err)
// the request to be signed later
var payload signinwithssb.ClientPayload
payload.ServerID = ts.NetworkInfo.RoomID
@ -339,8 +288,8 @@ func TestAuthWithSSBClientInitHasClient(t *testing.T) {
payload.ClientChallenge = cc
// prepare the url
urlTo := web.NewURLTo(ts.Router)
signInStartURL := urlTo(router.AuthWithSSBLogin,
signInStartURL := ts.URLTo(router.AuthWithSSBLogin,
"cid", client.Feed.Ref(),
"cc", cc,
)
@ -350,11 +299,10 @@ func TestAuthWithSSBClientInitHasClient(t *testing.T) {
r.NotNil(signInStartURL)
t.Log(signInStartURL.String())
doc, resp := ts.Client.GetHTML(signInStartURL.String())
doc, resp := ts.Client.GetHTML(signInStartURL)
a.Equal(http.StatusTemporaryRedirect, resp.Code)
dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL()
r.Nil(err)
dashboardURL := ts.URLTo(router.AdminDashboard)
a.Equal(dashboardURL.Path, resp.Header().Get("Location"))
webassert.Localized(t, doc, []webassert.LocalizedElement{
@ -373,31 +321,8 @@ func TestAuthWithSSBClientInitHasClient(t *testing.T) {
// check that we have a new cookie
sessionCookie := resp.Result().Cookies()
r.True(len(sessionCookie) > 0, "expecting one cookie!")
jar.SetCookies(signInStartURL, sessionCookie)
// now request the protected dashboard page
dashboardURL.Host = "localhost"
dashboardURL.Scheme = "https"
// load the cookie for the dashboard
cs := jar.Cookies(dashboardURL)
r.True(len(cs) > 0, "expecting one cookie!")
var sessionHeader = http.Header(map[string][]string{})
for _, c := range cs {
theCookie := c.String()
a.NotEqual("", theCookie, "should have a new cookie")
sessionHeader.Add("Cookie", theCookie)
}
durl := dashboardURL.String()
t.Log(durl)
// update headers
ts.Client.ClearHeaders()
ts.Client.SetHeaders(sessionHeader)
html, resp := ts.Client.GetHTML(durl)
html, resp := ts.Client.GetHTML(dashboardURL)
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
t.Log(html.Find("body").Text())
}
@ -421,13 +346,13 @@ func TestAuthWithSSBServerInitHappyPath(t *testing.T) {
ts.MembersDB.GetByFeedReturns(testMember, nil)
// prepare the url
urlTo := web.NewURLTo(ts.Router)
signInStartURL := urlTo(router.AuthWithSSBLogin,
signInStartURL := ts.URLTo(router.AuthWithSSBLogin,
"cid", client.Feed.Ref(),
)
r.NotNil(signInStartURL)
html, resp := ts.Client.GetHTML(signInStartURL.String())
html, resp := ts.Client.GetHTML(signInStartURL)
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
t.Log(html.Find("body").Text())
}
@ -476,8 +401,8 @@ func TestAuthWithSSBServerInitHappyPath(t *testing.T) {
}()
// start reading sse
sseURL := urlTo(router.AuthWithSSBServerEvents, "sc", serverChallenge)
resp = ts.Client.GetBody(sseURL.String())
sseURL := ts.URLTo(router.AuthWithSSBServerEvents, "sc", serverChallenge)
resp = ts.Client.GetBody(sseURL)
a.Equal(http.StatusOK, resp.Result().StatusCode)
// check contents of sse channel
@ -492,38 +417,14 @@ func TestAuthWithSSBServerInitHappyPath(t *testing.T) {
// use the token and go to /withssb/finalize and get a cookie
// (this happens in the browser engine via login-events.js)
finalizeURL := urlTo(router.AuthWithSSBFinalize, "token", testToken)
finalizeURL.Host = "localhost"
finalizeURL.Scheme = "https"
finalizeURL := ts.URLTo(router.AuthWithSSBFinalize, "token", testToken)
resp = ts.Client.GetBody(finalizeURL.String())
csrfCookie := resp.Result().Cookies()
a.Len(csrfCookie, 2, "csrf and session cookie")
// very cheap "browser" client session
jar, err := cookiejar.New(nil)
r.NoError(err)
jar.SetCookies(finalizeURL, csrfCookie)
resp = ts.Client.GetBody(finalizeURL)
// now request the protected dashboard page
dashboardURL, err := ts.Router.Get(router.AdminDashboard).URL()
r.Nil(err)
dashboardURL.Host = "localhost"
dashboardURL.Scheme = "https"
dashboardURL := ts.URLTo(router.AdminDashboard)
// load the cookie for the dashboard
cs := jar.Cookies(dashboardURL)
r.True(len(cs) > 0, "expecting one cookie!")
var sessionHeader = http.Header(map[string][]string{})
for _, c := range cs {
theCookie := c.String()
a.NotEqual("", theCookie, "should have a new cookie")
sessionHeader.Add("Cookie", theCookie)
}
ts.Client.SetHeaders(sessionHeader)
html, resp = ts.Client.GetHTML(dashboardURL.String())
html, resp = ts.Client.GetHTML(dashboardURL)
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
t.Log(html.Find("body").Text())
}
@ -547,13 +448,12 @@ func TestAuthWithSSBServerInitWrongSolution(t *testing.T) {
ts.MembersDB.GetByFeedReturns(testMember, nil)
// prepare the url
urlTo := web.NewURLTo(ts.Router)
signInStartURL := urlTo(router.AuthWithSSBLogin,
signInStartURL := ts.URLTo(router.AuthWithSSBLogin,
"cid", client.Feed.Ref(),
)
r.NotNil(signInStartURL)
html, resp := ts.Client.GetHTML(signInStartURL.String())
html, resp := ts.Client.GetHTML(signInStartURL)
if !a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for dashboard") {
t.Log(html.Find("body").Text())
}
@ -571,8 +471,8 @@ func TestAuthWithSSBServerInitWrongSolution(t *testing.T) {
}()
// start reading sse
sseURL := urlTo(router.AuthWithSSBServerEvents, "sc", serverChallenge)
resp = ts.Client.GetBody(sseURL.String())
sseURL := ts.URLTo(router.AuthWithSSBServerEvents, "sc", serverChallenge)
resp = ts.Client.GetBody(sseURL)
a.Equal(http.StatusOK, resp.Result().StatusCode)
// check contents of sse channel
@ -585,7 +485,7 @@ func TestAuthWithSSBServerInitWrongSolution(t *testing.T) {
a.True(strings.Contains(sseBody, "event: failed\n"), "success event")
// use an invalid token
finalizeURL := urlTo(router.AuthWithSSBFinalize, "token", "wrong")
resp = ts.Client.GetBody(finalizeURL.String())
finalizeURL := ts.URLTo(router.AuthWithSSBFinalize, "token", "wrong")
resp = ts.Client.GetBody(finalizeURL)
a.Equal(http.StatusForbidden, resp.Result().StatusCode)
}

View File

@ -4,10 +4,10 @@ package handlers
import (
"net/http"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
@ -17,11 +17,10 @@ func TestIndex(t *testing.T) {
ts := setup(t)
a := assert.New(t)
r := require.New(t)
url, err := ts.Router.Get(router.CompleteIndex).URL()
r.Nil(err)
html, resp := ts.Client.GetHTML(url.String())
url := ts.URLTo(router.CompleteIndex)
html, resp := ts.Client.GetHTML(url)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
webassert.Localized(t, html, []webassert.LocalizedElement{
{"h1", "Default Notice Title"},
@ -36,11 +35,10 @@ func TestAbout(t *testing.T) {
ts := setup(t)
a := assert.New(t)
r := require.New(t)
url, err := ts.Router.Get(router.CompleteAbout).URL()
r.Nil(err)
html, resp := ts.Client.GetHTML(url.String())
url := ts.URLTo(router.CompleteAbout)
html, resp := ts.Client.GetHTML(url)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
found := html.Find("h1").Text()
a.Equal("The about page", found)
@ -51,7 +49,10 @@ func TestNotFound(t *testing.T) {
a := assert.New(t)
html, resp := ts.Client.GetHTML("/some/random/ASDKLANZXC")
url404, err := url.Parse("/some/random/ASDKLANZXC")
a.NoError(err)
html, resp := ts.Client.GetHTML(url404)
a.Equal(http.StatusNotFound, resp.Code, "wrong HTTP status code")
found := html.Find("h1").Text()
a.Equal("Error #404 - Not Found", found)

View File

@ -85,26 +85,14 @@ func New(
roomsAuth.HTMLTemplates,
admin.HTMLTemplates,
)
allTheTemplates = append(allTheTemplates, "error.tmpl")
r, err := render.New(web.Templates,
renderOpts := []render.Option{
render.SetLogger(logger),
render.BaseTemplates("base.tmpl", "menu.tmpl"),
render.BaseTemplates("base.tmpl", "menu.tmpl", "flashes.tmpl"),
render.AddTemplates(allTheTemplates...),
// render.ErrorTemplate(),
render.SetErrorHandler(eh.Handle),
render.FuncMap(web.TemplateFuncs(m)),
// TODO: move these to the i18n helper pkg
render.InjectTemplateFunc("i18npl", func(r *http.Request) interface{} {
loc := i18n.LocalizerFromRequest(locHelper, r)
return loc.LocalizePlurals
}),
render.InjectTemplateFunc("i18n", func(r *http.Request) interface{} {
loc := i18n.LocalizerFromRequest(locHelper, r)
return loc.LocalizeSimple
}),
render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} {
return func(routeName string) bool {
route := m.Get(routeName)
@ -146,7 +134,10 @@ func New(
}),
render.InjectTemplateFunc("is_logged_in", members.TemplateHelper()),
)
}
renderOpts = append(renderOpts, locHelper.GetRenderFuncs()...)
r, err := render.New(web.Templates, renderOpts...)
if err != nil {
return nil, fmt.Errorf("web Handler: failed to create renderer: %w", err)
}
@ -165,6 +156,8 @@ func New(
},
}
flashHelper := weberrs.NewFlashHelper(cookieStore, locHelper)
authWithPassword, err := auth.NewHandler(dbs.AuthFallback,
auth.SetStore(cookieStore),
auth.SetErrorHandler(func(rw http.ResponseWriter, req *http.Request, err error, code int) {
@ -238,6 +231,7 @@ func New(
netInfo.Domain,
r,
roomState,
flashHelper,
admin.Databases{
Aliases: dbs.Aliases,
Config: dbs.Config,

View File

@ -5,7 +5,6 @@ import (
"encoding/base64"
"encoding/json"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"testing"
@ -14,7 +13,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/ssb-ngi-pointer/go-ssb-room/web/webassert"
@ -24,22 +22,18 @@ import (
func TestInviteShowAcceptForm(t *testing.T) {
ts := setup(t)
urlTo := web.NewURLTo(ts.Router)
t.Run("token doesnt exist", func(t *testing.T) {
a, r := assert.New(t), require.New(t)
testToken := "nonexistant-test-token"
acceptURL404 := urlTo(router.CompleteInviteFacade, "token", testToken)
acceptURL404 := ts.URLTo(router.CompleteInviteFacade, "token", testToken)
r.NotNil(acceptURL404)
// prep the mocked db for http:404
ts.InvitesDB.GetByTokenReturns(roomdb.Invite{}, roomdb.ErrNotFound)
// request the form
acceptForm := acceptURL404.String()
t.Log(acceptForm)
doc, resp := ts.Client.GetHTML(acceptForm)
doc, resp := ts.Client.GetHTML(acceptURL404)
// 500 until https://github.com/ssb-ngi-pointer/go-ssb-room/issues/66 is fixed
a.Equal(http.StatusInternalServerError, resp.Code)
@ -62,17 +56,14 @@ func TestInviteShowAcceptForm(t *testing.T) {
a, r := assert.New(t), require.New(t)
testToken := "existing-test-token"
facadeURL := urlTo(router.CompleteInviteFacade, "token", testToken)
r.NotNil(facadeURL)
validAcceptURL := ts.URLTo(router.CompleteInviteFacade, "token", testToken)
// prep the mocked db for http:200
fakeExistingInvite := roomdb.Invite{ID: 1234}
ts.InvitesDB.GetByTokenReturns(fakeExistingInvite, nil)
// request the form
validAcceptForm := facadeURL.String()
t.Log(validAcceptForm)
doc, resp := ts.Client.GetHTML(validAcceptForm)
doc, resp := ts.Client.GetHTML(validAcceptURL)
a.Equal(http.StatusOK, resp.Code)
// check database calls
@ -91,9 +82,10 @@ func TestInviteShowAcceptForm(t *testing.T) {
a.True(ok)
// Fallback URL in data-href-fallback
fallbackURL := urlTo(router.CompleteInviteFacadeFallback, "token", testToken)
fallbackURL := ts.URLTo(router.CompleteInviteFacadeFallback, "token", testToken)
want := fallbackURL.Path + "?" + fallbackURL.RawQuery
joinDataHrefFallback, ok := doc.Find("#join-room-uri").Attr("data-href-fallback")
a.Equal(fallbackURL.String(), joinDataHrefFallback)
a.Equal(want, joinDataHrefFallback)
a.True(ok)
// ssb-uri in data-href
@ -112,10 +104,7 @@ func TestInviteShowAcceptForm(t *testing.T) {
a.Equal(testToken, inviteParam)
postTo := params.Get("postTo")
expectedConsumeInviteURL, err := ts.Router.Get(router.CompleteInviteConsume).URL()
expectedConsumeInviteURL.Scheme = "https"
expectedConsumeInviteURL.Host = "localhost"
r.NoError(err)
expectedConsumeInviteURL := ts.URLTo(router.CompleteInviteConsume)
a.Equal(expectedConsumeInviteURL.String(), postTo)
})
}
@ -123,41 +112,30 @@ func TestInviteShowAcceptForm(t *testing.T) {
func TestInviteConsumeInviteHTTP(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
urlTo := web.NewURLTo(ts.Router)
testToken := "existing-test-token-2"
validAcceptURL := urlTo(router.CompleteInviteInsertID, "token", testToken)
r.NotNil(validAcceptURL)
validAcceptURL.Host = "localhost"
validAcceptURL.Scheme = "https"
validAcceptURL := ts.URLTo(router.CompleteInviteInsertID, "token", testToken)
testInvite := roomdb.Invite{ID: 4321}
ts.InvitesDB.GetByTokenReturns(testInvite, nil)
// request the form (for a valid csrf token)
validAcceptForm := validAcceptURL.String()
t.Log(validAcceptForm)
doc, resp := ts.Client.GetHTML(validAcceptForm)
doc, resp := ts.Client.GetHTML(validAcceptURL)
a.Equal(http.StatusOK, resp.Code)
form := doc.Find("form#consume")
r.Equal(1, form.Length())
consumeInviteURLString, has := form.Attr("action")
a.True(has, "form should have an action attribute")
expectedConsumeInviteURL := ts.URLTo(router.CompleteInviteConsume)
a.Equal(expectedConsumeInviteURL.Path, consumeInviteURLString)
webassert.CSRFTokenPresent(t, form)
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "invite", Type: "hidden", Value: testToken},
{Name: "id", Type: "text"},
})
// we need a functional jar to unpack the Set-Cookie response for the csrf token
jar, err := cookiejar.New(nil)
r.NoError(err)
// update the jar
csrfCookie := resp.Result().Cookies()
a.Len(csrfCookie, 1, "should have one cookie for CSRF protection validation")
jar.SetCookies(validAcceptURL, csrfCookie)
// get the corresponding token from the page
csrfTokenElem := doc.Find("input[name='gorilla.csrf.Token']")
a.Equal(1, csrfTokenElem.Length())
@ -179,31 +157,18 @@ func TestInviteConsumeInviteHTTP(t *testing.T) {
}
// construct the consume endpoint url
consumeInviteURLString, has := form.Attr("action")
a.True(has, "form should have an action attribute")
expectedConsumeInviteURL, err := ts.Router.Get(router.CompleteInviteConsume).URL()
r.Nil(err)
a.Equal(expectedConsumeInviteURL.String(), consumeInviteURLString)
consumeInviteURL, err := url.Parse(consumeInviteURLString)
r.Nil(err)
consumeInviteURL.Host = "localhost"
consumeInviteURL.Scheme = "https"
consumeInviteURL := ts.URLTo(router.CompleteInviteConsume)
// construct the header with Referer and Cookie
// construct the header with the Referer or csrf check
var csrfCookieHeader = http.Header(map[string][]string{})
csrfCookieHeader.Set("Referer", "https://localhost")
cs := jar.Cookies(consumeInviteURL)
r.Len(cs, 1, "expecting one cookie for csrf")
theCookie := cs[0].String()
a.NotEqual("", theCookie, "should have a new cookie")
csrfCookieHeader.Set("Cookie", theCookie)
ts.Client.SetHeaders(csrfCookieHeader)
// prepare the mock
ts.InvitesDB.ConsumeReturns(testInvite, nil)
// send the POST
resp = ts.Client.PostForm(consumeInviteURL.String(), consumeVals)
resp = ts.Client.PostForm(consumeInviteURL, consumeVals)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for sign in")
// check how consume was called
@ -216,11 +181,8 @@ func TestInviteConsumeInviteHTTP(t *testing.T) {
func TestInviteConsumeInviteJSON(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
urlTo := web.NewURLTo(ts.Router)
testToken := "existing-test-token-2"
validAcceptURL := urlTo(router.CompleteInviteFacade, "token", testToken)
r.NotNil(validAcceptURL)
testInvite := roomdb.Invite{ID: 4321}
ts.InvitesDB.GetByTokenReturns(testInvite, nil)
@ -236,14 +198,14 @@ func TestInviteConsumeInviteJSON(t *testing.T) {
consume.ID = testNewMember
// construct the consume endpoint url
consumeInviteURL := urlTo(router.CompleteInviteConsume)
consumeInviteURL := ts.URLTo(router.CompleteInviteConsume)
r.NotNil(consumeInviteURL)
// prepare the mock
ts.InvitesDB.ConsumeReturns(testInvite, nil)
// send the POST
resp := ts.Client.SendJSON(consumeInviteURL.String(), consume)
resp := ts.Client.SendJSON(consumeInviteURL, consume)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for sign in")
// check how consume was called

View File

@ -2,15 +2,12 @@ package handlers
import (
"net/http"
"net/http/cookiejar"
"net/url"
"testing"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestNoticeSmokeTest ensures the most basic notice serving is working
@ -25,7 +22,8 @@ func TestNoticeSmokeTest(t *testing.T) {
ts.NoticeDB.GetByIDReturns(noticeData, nil)
html, res := ts.Client.GetHTML("/notice/show?id=1")
noticeURL := ts.URLTo(router.CompleteNoticeShow, "id", "1")
html, res := ts.Client.GetHTML(noticeURL)
a.Equal(http.StatusOK, res.Code, "wrong HTTP status code")
a.Equal("Welcome!", html.Find("title").Text())
}
@ -47,7 +45,8 @@ Hello world!
ts.NoticeDB.GetByIDReturns(noticeData, nil)
html, res := ts.Client.GetHTML("/notice/show?id=1")
noticeURL := ts.URLTo(router.CompleteNoticeShow, "id", "1")
html, res := ts.Client.GetHTML(noticeURL)
a.Equal(http.StatusOK, res.Code, "wrong HTTP status code")
a.Equal("Welcome!", html.Find("title").Text())
a.Equal("The loveliest of rooms is here", html.Find("h2").Text())
@ -57,9 +56,7 @@ Hello world!
// then we log in as an admin and see that the edit links are there.
func TestNoticesEditButtonVisible(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
urlTo := web.NewURLTo(ts.Router)
a := assert.New(t)
ts.AliasesDB.ResolveReturns(roomdb.Alias{}, roomdb.ErrNotFound)
@ -71,12 +68,11 @@ func TestNoticesEditButtonVisible(t *testing.T) {
ts.NoticeDB.GetByIDReturns(noticeData, nil)
// first, we confirm that the button is missing when not logged in
noticeURL := urlTo(router.CompleteNoticeShow, "id", 42)
noticeURL.Host = "localhost"
noticeURL.Scheme = "https"
noticeURL := ts.URLTo(router.CompleteNoticeShow, "id", 42)
editButtonSelector := `#edit-notice`
doc, resp := ts.Client.GetHTML(noticeURL.String())
doc, resp := ts.Client.GetHTML(noticeURL)
a.Equal(http.StatusOK, resp.Code)
// empty selection <=> we have no link
@ -84,25 +80,17 @@ func TestNoticesEditButtonVisible(t *testing.T) {
// start preparing the ~login dance~
// TODO: make this code reusable and share it with the login => /dashboard http:200 test
// cookiejar: a very cheap client session
// TODO: refactor login dance for re-use in testing / across tests
jar, err := cookiejar.New(nil)
r.NoError(err)
// when dealing with cookies we also need to have an Host and URL-Scheme
// for the jar to save and load them correctly
formEndpoint := urlTo(router.AuthFallbackLogin)
r.NotNil(formEndpoint)
formEndpoint.Host = "localhost"
formEndpoint.Scheme = "https"
formEndpoint := ts.URLTo(router.AuthFallbackLogin)
doc, resp = ts.Client.GetHTML(formEndpoint.String())
doc, resp = ts.Client.GetHTML(formEndpoint)
a.Equal(http.StatusOK, resp.Code)
csrfCookie := resp.Result().Cookies()
a.Len(csrfCookie, 1, "should have one cookie for CSRF protection validation")
t.Log(csrfCookie)
jar.SetCookies(formEndpoint, csrfCookie)
a.True(len(csrfCookie) > 0, "should have one cookie for CSRF protection validation")
csrfTokenElem := doc.Find("input[type=hidden]")
a.Equal(1, csrfTokenElem.Length())
@ -125,44 +113,19 @@ func TestNoticesEditButtonVisible(t *testing.T) {
ts.AuthFallbackDB.CheckReturns(testUser.ID, nil)
ts.MembersDB.GetByIDReturns(testUser, nil)
postEndpoint, err := ts.Router.Get(router.AuthFallbackFinalize).URL()
r.Nil(err)
postEndpoint.Host = "localhost"
postEndpoint.Scheme = "https"
postEndpoint := ts.URLTo(router.AuthFallbackFinalize)
// construct HTTP Header with Referer and Cookie
var csrfCookieHeader = http.Header(map[string][]string{})
csrfCookieHeader.Set("Referer", "https://localhost")
cs := jar.Cookies(postEndpoint)
r.Len(cs, 1, "expecting one cookie for csrf")
theCookie := cs[0].String()
a.NotEqual("", theCookie, "should have a new cookie")
csrfCookieHeader.Set("Cookie", theCookie)
ts.Client.SetHeaders(csrfCookieHeader)
var refererHeader = make(http.Header)
refererHeader.Set("Referer", "https://localhost")
ts.Client.SetHeaders(refererHeader)
resp = ts.Client.PostForm(postEndpoint.String(), loginVals)
resp = ts.Client.PostForm(postEndpoint, loginVals)
a.Equal(http.StatusSeeOther, resp.Code, "wrong HTTP status code for sign in")
sessionCookie := resp.Result().Cookies()
jar.SetCookies(postEndpoint, sessionCookie)
var sessionHeader = http.Header(map[string][]string{})
cs = jar.Cookies(noticeURL)
// TODO: why doesnt this return the csrf cookie?!
r.NotEqual(len(cs), 0, "expecting a cookie!")
for _, c := range cs {
theCookie := c.String()
a.NotEqual("", theCookie, "should have a new cookie")
sessionHeader.Add("Cookie", theCookie)
}
// update headers
ts.Client.ClearHeaders()
ts.Client.SetHeaders(sessionHeader)
cnt := ts.MembersDB.GetByIDCallCount()
// now we are logged in, anchor tag should be there
doc, resp = ts.Client.GetHTML(noticeURL.String())
doc, resp = ts.Client.GetHTML(noticeURL)
a.Equal(http.StatusOK, resp.Code)
a.Equal(cnt+1, ts.MembersDB.GetByIDCallCount())

View File

@ -5,15 +5,12 @@ package handlers
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"testing"
"github.com/BurntSushi/toml"
"github.com/gorilla/mux"
"go.mindeco.de/http/tester"
"go.mindeco.de/logging/logtest"
@ -24,7 +21,8 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n/i18ntesting"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
refs "go.mindeco.de/ssb-refs"
)
@ -32,7 +30,7 @@ import (
type testSession struct {
Mux *http.ServeMux
Client *tester.Tester
Router *mux.Router
URLTo web.URLMaker
// mocked dbs
AuthDB *mockdb.FakeAuthWithSSBService
@ -55,8 +53,6 @@ type testSession struct {
NetworkInfo network.ServerEndpointDetails
}
var testI18N = justTheKeys()
func setup(t *testing.T) *testSession {
t.Parallel()
var ts testSession
@ -65,12 +61,7 @@ func setup(t *testing.T) *testSession {
os.RemoveAll(testRepoPath)
testRepo := repo.New(testRepoPath)
testOverride := filepath.Join(testRepo.GetPath("i18n"), "active.en.toml")
os.MkdirAll(filepath.Dir(testOverride), 0700)
err := ioutil.WriteFile(testOverride, testI18N, 0700)
if err != nil {
t.Fatal(err)
}
i18ntesting.WriteReplacement(t)
ts.AuthDB = new(mockdb.FakeAuthWithSSBService)
ts.AuthFallbackDB = new(mockdb.FakeAuthFallbackService)
@ -107,7 +98,18 @@ func setup(t *testing.T) *testSession {
ctx := context.TODO()
ts.RoomState = roomstate.NewManager(ctx, log)
ts.Router = router.CompleteApp()
// instantiate the urlTo helper (constructs urls for us!)
// the cookiejar in our custom http/tester needs a non-empty domain and scheme
urlTo := web.NewURLTo(router.CompleteApp())
ts.URLTo = func(name string, vals ...interface{}) *url.URL {
testURL := urlTo(name, vals...)
if testURL == nil {
t.Fatalf("no URL for %s", name)
}
testURL.Host = ts.NetworkInfo.Domain
testURL.Scheme = "https" // fake
return testURL
}
ts.SignalBridge = signinwithssb.NewSignalBridge()
@ -140,55 +142,3 @@ func setup(t *testing.T) *testSession {
return &ts
}
// auto generate from defaults a list of Label = "Label"
// must keep order of input intact
// (at least all the globals before starting with nested plurals)
// also replaces 'one' and 'other' in plurals
func justTheKeys() []byte {
f, err := i18n.Defaults.Open("active.en.toml")
if err != nil {
panic(err)
}
justAMap := make(map[string]interface{})
md, err := toml.DecodeReader(f, &justAMap)
if err != nil {
panic(err)
}
var buf = &bytes.Buffer{}
// if we don't produce the same order as the input
// (in go maps are ALWAYS random access when ranged over)
// nested keys (such as plural form) will mess up the global level...
for _, k := range md.Keys() {
key := k.String()
val, has := justAMap[key]
if !has {
// fmt.Println("i18n test warning:", key, "not unmarshaled")
continue
}
switch tv := val.(type) {
case string:
fmt.Fprintf(buf, "%s = \"%s\"\n", key, key)
case map[string]interface{}:
// fmt.Println("i18n test warning: custom map for ", key)
fmt.Fprintf(buf, "\n[%s]\n", key)
// replace "one" and "other" keys
// with Label and LabelPlural
tv["one"] = key + "Singular"
tv["other"] = key + "Plural"
toml.NewEncoder(buf).Encode(tv)
fmt.Fprintln(buf)
default:
panic(fmt.Sprintf("unhandled toml structure under %s: %T\n", key, val))
}
}
return buf.Bytes()
}

View File

@ -88,6 +88,7 @@ Settings = "Settings"
AdminDeniedKeysTitle = "Banned"
AdminDeniedKeysWelcome = "This page can be used to ban SSB IDs so that they can't access the room any more."
AdminDeniedKeysAdd = "Add"
AdminDeniedKeysAdded = "Key was added to the list."
AdminDeniedKeysRemove = "Remove"
AdminDeniedKeysComment = "Comment"
AdminDeniedKeysCommentDescription = "The person who added this ban, added the following comment"
@ -112,6 +113,10 @@ AdminMemberDetailsAliasRevoke = "Revoke"
AdminMemberDetailsExclusion = "Exclusion from this room"
AdminMemberDetailsRemove = "Remove member"
AdminMemberAdded = "Member added successfully."
AdminMemberUpdated = "Member updated."
AdminMemberRemoved = "Member removed."
# invite dashboard
##################

View File

@ -15,6 +15,7 @@ import (
"github.com/BurntSushi/toml"
"github.com/nicksnyder/go-i18n/v2/i18n"
"go.mindeco.de/http/render"
"golang.org/x/text/language"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
@ -116,11 +117,26 @@ func New(r repo.Interface) (*Helper, error) {
return &Helper{bundle: bundle}, nil
}
func (h Helper) GetRenderFuncs() []render.Option {
var opts = []render.Option{
render.InjectTemplateFunc("i18npl", func(r *http.Request) interface{} {
loc := h.FromRequest(r)
return loc.LocalizePlurals
}),
render.InjectTemplateFunc("i18n", func(r *http.Request) interface{} {
loc := h.FromRequest(r)
return loc.LocalizeSimple
}),
}
return opts
}
type Localizer struct {
loc *i18n.Localizer
}
func (h Helper) NewLocalizer(lang string, accept ...string) *Localizer {
func (h Helper) newLocalizer(lang string, accept ...string) *Localizer {
var langs = []string{lang}
langs = append(langs, accept...)
var l Localizer
@ -128,6 +144,15 @@ func (h Helper) NewLocalizer(lang string, accept ...string) *Localizer {
return &l
}
// FromRequest returns a new Localizer for the passed helper,
// using form value 'lang' and Accept-Language http header from the passed request.
// TODO: user settings/cookie values?
func (h Helper) FromRequest(r *http.Request) *Localizer {
lang := r.FormValue("lang")
accept := r.Header.Get("Accept-Language")
return h.newLocalizer(lang, accept)
}
func (l Localizer) LocalizeSimple(messageID string) string {
msg, err := l.loc.Localize(&i18n.LocalizeConfig{
MessageID: messageID,
@ -178,12 +203,3 @@ func (l Localizer) LocalizePluralsWithData(messageID string, pluralCount int, tp
panic(fmt.Sprintf("i18n/error: failed to localize label %s: %s", messageID, err))
}
// LocalizerFromRequest returns a new Localizer for the passed helper,
// using form value 'lang' and Accept-Language http header from the passed request.
// TODO: user settings/cookie values?
func LocalizerFromRequest(helper *Helper, r *http.Request) *Localizer {
lang := r.FormValue("lang")
accept := r.Header.Get("Accept-Language")
return helper.NewLocalizer(lang, accept)
}

View File

@ -0,0 +1,82 @@
package i18ntesting
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/BurntSushi/toml"
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
)
// justTheKeys auto generates from the defaults a list of Label = "Label"
// must keep order of input intact
// (at least all the globals before starting with nested plurals)
// also replaces 'one' and 'other' in plurals
func justTheKeys(t *testing.T) []byte {
f, err := i18n.Defaults.Open("active.en.toml")
if err != nil {
t.Fatal(err)
}
justAMap := make(map[string]interface{})
md, err := toml.DecodeReader(f, &justAMap)
if err != nil {
t.Fatal(err)
}
var buf = &bytes.Buffer{}
// if we don't produce the same order as the input
// (in go maps are ALWAYS random access when ranged over)
// nested keys (such as plural form) will mess up the global level...
for _, k := range md.Keys() {
key := k.String()
val, has := justAMap[key]
if !has {
// fmt.Println("i18n test warning:", key, "not unmarshaled")
continue
}
switch tv := val.(type) {
case string:
fmt.Fprintf(buf, "%s = \"%s\"\n", key, key)
case map[string]interface{}:
// fmt.Println("i18n test warning: custom map for ", key)
fmt.Fprintf(buf, "\n[%s]\n", key)
// replace "one" and "other" keys
// with Label and LabelPlural
tv["one"] = key + "Singular"
tv["other"] = key + "Plural"
toml.NewEncoder(buf).Encode(tv)
fmt.Fprintln(buf)
default:
t.Fatalf("unhandled toml structure under %s: %T\n", key, val)
}
}
return buf.Bytes()
}
func WriteReplacement(t *testing.T) {
r := repo.New(filepath.Join("testrun", t.Name()))
testOverride := filepath.Join(r.GetPath("i18n"), "active.en.toml")
t.Log(testOverride)
os.MkdirAll(filepath.Dir(testOverride), 0700)
content := justTheKeys(t)
err := ioutil.WriteFile(testOverride, content, 0700)
if err != nil {
t.Fatal(err)
}
}

View File

@ -6,6 +6,8 @@
<p id="welcome" class="my-2">{{i18n "AdminDeniedKeysWelcome"}}</p>
{{ template "flashes" . }}
<p
id="DeniedKeysCount"
class="text-lg font-bold my-2"

View File

@ -6,6 +6,8 @@
<p id="welcome" class="my-2">{{i18n "AdminMembersWelcome"}}</p>
{{ template "flashes" . }}
<form
id="add-entry"
action="{{urlTo "admin:members:add"}}"

View File

@ -0,0 +1,11 @@
{{ define "flashes" }}
{{if .Flashes}}
<ul id="flashes-list">
{{range .Flashes}}
<li
class="{{if eq .Kind 1}}text-red-600{{else}}text-green-600{{end}}"
>{{.Message}}</li>
{{end}}
</ul>
{{end}}
{{ end }}

View File

@ -34,12 +34,14 @@ func TemplateFuncs(m *mux.Router) template.FuncMap {
}
}
type URLMaker func(string, ...interface{}) *url.URL
// NewURLTo returns a template helper function for a router.
// It is usually called with one parameter, the route name, which should be defined in the router package.
// If it's called with more then one, it has a to be a pair of two values. (1, 3, 5, 7, etc.)
// The first value of such a pair is the placeholder name in the router (i.e. in '/our/routes/{id:[0-9]+}/test' it would be id )
// and the 2nd value is the actual value that should be put in place of the placeholder.
func NewURLTo(appRouter *mux.Router) func(string, ...interface{}) *url.URL {
func NewURLTo(appRouter *mux.Router) URLMaker {
l := logging.Logger("helper.URLTo") // TOOD: inject in a scoped way
return func(routeName string, ps ...interface{}) *url.URL {
route := appRouter.Get(routeName)