invite form testing

This commit is contained in:
Henry 2021-03-03 13:58:06 +01:00
parent bbcab73cb5
commit c7bcef4339
13 changed files with 283 additions and 2 deletions

View File

@ -217,6 +217,7 @@ func runroomsrv() error {
db.AuthWithSSB,
db.AuthFallback,
db.AllowList,
db.Invites,
db.Notices,
db.PinnedNotices,
)

View File

@ -76,6 +76,8 @@ func (h allowListH) overview(rw http.ResponseWriter, req *http.Request) (interfa
}
count := len(lst)
// TODO: generalize paginator code
num, err := strconv.ParseInt(req.URL.Query().Get("page"), 10, 32)
if err != nil {
num = 1

View File

@ -64,6 +64,7 @@ func TestAllowListAdd(t *testing.T) {
a.EqualValues(1, inputSelection.Length())
name, ok := inputSelection.Attr("name")
a.True(ok, "field has a name")
a.Equal("pub_key", name, "wrong name on input field")
newKey := "@x7iOLUcq3o+sjGeAnipvWeGzfuYgrXl8L4LYlxIhwDc=.ed25519"

View File

@ -28,6 +28,7 @@ type testSession struct {
AllowListDB *mockdb.FakeAllowListService
PinnedDB *mockdb.FakePinnedNoticesService
NoticeDB *mockdb.FakeNoticesService
InvitesDB *mockdb.FakeInviteService
RoomState *roomstate.Manager
}
@ -39,6 +40,7 @@ func newSession(t *testing.T) *testSession {
ts.AllowListDB = new(mockdb.FakeAllowListService)
ts.PinnedDB = new(mockdb.FakePinnedNoticesService)
ts.NoticeDB = new(mockdb.FakeNoticesService)
ts.InvitesDB = new(mockdb.FakeInviteService)
log, _ := logtest.KitLogger("admin", t)
ctx := context.TODO()
@ -75,7 +77,13 @@ func newSession(t *testing.T) *testSession {
}
ts.Mux = http.NewServeMux()
ts.Mux.Handle("/", Handler(r, ts.RoomState, ts.AllowListDB, ts.NoticeDB, ts.PinnedDB))
ts.Mux.Handle("/", Handler(r,
ts.RoomState,
ts.AllowListDB,
ts.InvitesDB,
ts.NoticeDB,
ts.PinnedDB,
))
ts.Client = tester.New(ts.Mux, t)
return &ts

View File

@ -21,6 +21,8 @@ var HTMLTemplates = []string{
"admin/allow-list.tmpl",
"admin/allow-list-remove-confirm.tmpl",
"admin/invites.tmpl",
"admin/notice-edit.tmpl",
}
@ -30,6 +32,7 @@ func Handler(
r *render.Renderer,
roomState *roomstate.Manager,
al admindb.AllowListService,
is admindb.InviteService,
ndb admindb.NoticesService,
pdb admindb.PinnedNoticesService,
) http.Handler {
@ -57,6 +60,13 @@ func Handler(
mux.HandleFunc("/members/remove/confirm", r.HTML("admin/allow-list-remove-confirm.tmpl", ah.removeConfirm))
mux.HandleFunc("/members/remove", ah.remove)
var ih = invitesH{
r: r,
db: is,
}
mux.HandleFunc("/invites", r.HTML("admin/invites.tmpl", ih.overview))
var nh = noticeHandler{
r: r,
noticeDB: ndb,

View File

@ -0,0 +1,84 @@
package admin
import (
"fmt"
"net/http"
"strconv"
"go.mindeco.de/http/render"
"github.com/gorilla/csrf"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
"github.com/vcraescu/go-paginator/v2"
"github.com/vcraescu/go-paginator/v2/adapter"
"github.com/vcraescu/go-paginator/v2/view"
)
type invitesH struct {
r *render.Renderer
db admindb.InviteService
}
func (h invitesH) overview(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
lst, err := h.db.List(req.Context())
if err != nil {
return nil, err
}
// Reverse the slice to provide recent-to-oldest results
for i, j := 0, len(lst)-1; i < j; i, j = i+1, j-1 {
lst[i], lst[j] = lst[j], lst[i]
}
// TODO: generalize paginator code
count := len(lst)
num, err := strconv.ParseInt(req.URL.Query().Get("page"), 10, 32)
if err != nil {
num = 1
}
page := int(num)
if page < 1 {
page = 1
}
paginator := paginator.New(adapter.NewSliceAdapter(lst), pageSize)
paginator.SetPage(page)
var entries admindb.ListEntries
if err = paginator.Results(&entries); err != nil {
return nil, fmt.Errorf("paginator failed with %w", err)
}
view := view.New(paginator)
pagesSlice, err := view.Pages()
if err != nil {
return nil, fmt.Errorf("paginator view.Pages failed with %w", err)
}
if len(pagesSlice) == 0 {
pagesSlice = []int{1}
}
last, err := view.Last()
if err != nil {
return nil, fmt.Errorf("paginator view.Last failed with %w", err)
}
firstInView := pagesSlice[0] == 1
lastInView := false
for _, num := range pagesSlice {
if num == last {
lastInView = true
}
}
return map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(req),
"Entries": entries,
"Count": count,
"Paginator": paginator,
"View": view,
"FirstInView": firstInView,
"LastInView": lastInView,
}, nil
}

View File

@ -0,0 +1,47 @@
package admin
import (
"net/http"
"testing"
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
"github.com/stretchr/testify/assert"
)
func TestInvitesCreateForm(t *testing.T) {
ts := newSession(t)
a := assert.New(t)
url, err := ts.Router.Get(router.AdminInvitesOverview).URL()
a.Nil(err)
html, resp := ts.Client.GetHTML(url.String())
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code")
assertLocalized(t, html, []localizedElement{
{"#welcome", "AdminInvitesWelcome"},
{"title", "AdminInvitesTitle"},
})
formSelection := html.Find("form#create-invite")
a.EqualValues(1, formSelection.Length())
method, ok := formSelection.Attr("method")
a.True(ok, "form has method set")
a.Equal("POST", method)
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)
inputSelection := formSelection.Find("input[type=text]")
a.EqualValues(1, inputSelection.Length())
name, ok := inputSelection.Attr("name")
a.True(ok, "input has a name")
a.Equal("alias_suggestion", name, "wrong name on input field")
}

View File

@ -44,6 +44,7 @@ func New(
as admindb.AuthWithSSBService,
fs admindb.AuthFallbackService,
al admindb.AllowListService,
is admindb.InviteService,
ns admindb.NoticesService,
ps admindb.PinnedNoticesService,
) (http.Handler, error) {
@ -221,7 +222,12 @@ func New(
// hookup handlers to the router
roomsAuth.Handler(m, r, a)
adminHandler := a.Authenticate(admin.Handler(r, roomState, al, ns, ps))
adminHandler := a.Authenticate(admin.Handler(r,
roomState,
al,
is,
ns,
ps))
mainMux.Handle("/admin/", adminHandler)
m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {

View File

@ -113,6 +113,8 @@ func TestFallbackAuth(t *testing.T) {
doc, resp := ts.Client.GetHTML(signInFormURL.String())
a.Equal(http.StatusOK, resp.Code)
assertCSRFTokenPresent(t, doc.Find("form"))
csrfCookie := resp.Result().Cookies()
a.Len(csrfCookie, 1, "should have one cookie for CSRF protection validation")
t.Log(csrfCookie)
@ -233,3 +235,13 @@ func assertLocalized(t *testing.T, html *goquery.Document, elems []localizedElem
a.Equal(pair.Label, html.Find(pair.Selector).Text(), "localized pair %d failed", i+1)
}
}
func assertCSRFTokenPresent(t *testing.T, sel *goquery.Selection) {
a := assert.New(t)
csrfField := sel.Find("input[name=gorilla.csrf.Token]")
a.EqualValues(1, csrfField.Length(), "no csrf-token input tag")
tipe, ok := csrfField.Attr("type")
a.True(ok, "csrf input has a type")
a.Equal("hidden", tipe, "wrong type on csrf field")
}

View File

@ -23,8 +23,14 @@ AdminAllowListRemove = "Remove"
AdminAllowListRemoveConfirmWelcome = "Are you sure you want to remove this member? They will lose their alias, if they have one."
AdminAllowListRemoveConfirmTitle = "Confirm member removal"
AdminInvitesTitle = "Invites"
AdminInvitesWelcome = "Here ytou can create invite tokens for people who are not yet members of this room."
AdminInvitesAliasSuggestion = "Suggested alias (optional)"
AdminInvitesRevoke = "Revoke"
NavAdminLanding = "Home"
NavAdminDashboard = "Dashboard"
NavAdminInvites = "Invites"
NavAdminNotices = "Notices"
NoticeEditTitle = "Edit Notice"

View File

@ -14,6 +14,9 @@ const (
AdminAllowListRemoveConfirm = "admin:allow-list:remove:confirm"
AdminAllowListRemove = "admin:allow-list:remove"
AdminInvitesOverview = "admin:invites:overview"
AdminInvitesCreate = "admin:invites:create"
AdminNoticeEdit = "admin:notice:edit"
AdminNoticeSave = "admin:notice:save"
AdminNoticeDraftTranslation = "admin:notice:translation:draft"
@ -39,5 +42,8 @@ func Admin(m *mux.Router) *mux.Router {
m.Path("/notice/translation/add").Methods("POST").Name(AdminNoticeAddTranslation)
m.Path("/notice/save").Methods("POST").Name(AdminNoticeSave)
m.Path("/invites").Methods("GET").Name(AdminInvitesOverview)
m.Path("/invites/create").Methods("POST").Name(AdminInvitesCreate)
return m
}

View File

@ -0,0 +1,89 @@
{{ define "title" }}{{i18n "AdminInvitesTitle"}}{{ end }}
{{ define "content" }}
<h1
class="text-3xl tracking-tight font-black text-black mt-2 mb-4"
>{{i18n "AdminInvitesTitle"}}</h1>
<p id="welcome" class="my-2">{{i18n "AdminInvitesWelcome"}}</p>
<p
id="inviteListCount"
class="text-lg font-bold my-2"
>{{i18npl "ListCount" .Count}}</p>
<ul id="theList" class="divide-y pb-4">
<form
id="create-invite"
action="{{urlTo "admin:invites:create"}}"
method="POST"
>
{{ .csrfField }}
<div class="flex flex-row items-center h-12">
<input
type="text"
name="alias_suggestion"
placeholder="{{i18n "AdminInvitesAliasSuggestion"}}"
class="font-mono truncate flex-auto tracking-wider h-12 text-gray-900 focus:outline-none focus:ring-1 focus:ring-green-500 focus:border-transparent placeholder-gray-300"
>
<input
type="submit"
value="{{i18n "GenericSave"}}"
class="pl-4 w-20 py-2 text-center text-green-500 hover:text-green-600 font-bold bg-transparent cursor-pointer"
>
</div>
</form>
{{range .Entries}}
<li class="flex flex-row items-center h-12">
<span
class="font-mono truncate flex-auto text-gray-600 tracking-wider"
>{{.CreatedBy.Name}}</span>
<a
href="{{urlTo "admin:invites:revoke:confirm" "id" .ID}}"
class="pl-4 w-20 py-2 text-center text-gray-400 hover:text-red-600 font-bold cursor-pointer"
>{{i18n "AdminInvitesRevoke"}}</a>
</li>
{{end}}
</ul>
{{$pageNums := .Paginator.PageNums}}
{{$view := .View}}
{{if gt $pageNums 1}}
<div class="flex flex-row justify-center">
{{if not .FirstInView}}
<a
href="{{urlTo "admin:invites:overview"}}?page=1"
class="rounded px-3 py-2 text-pink-600 border-transparent hover:border-pink-400 border-2"
>1</a>
<span
class="px-3 py-2 text-gray-400 border-2 border-transparent"
>..</span>
{{end}}
{{range $view.Pages}}
{{if le . $pageNums}}
{{if eq . $view.Current}}
<span
class="px-3 py-2 cursor-default text-gray-500 border-2 border-transparent"
>{{.}}</span>
{{else}}
<a
href="{{urlTo "admin:invites:overview"}}?page={{.}}"
class="rounded px-3 py-2 mx-1 text-pink-600 border-transparent hover:border-pink-400 border-2"
>{{.}}</a>
{{end}}
{{end}}
{{end}}
{{if not .LastInView}}
<span
class="px-3 py-2 text-gray-400 border-2 border-transparent"
>..</span>
<a
href="{{urlTo "admin:invites:overview"}}?page={{$view.Last}}"
class="rounded px-3 py-2 text-pink-600 border-transparent hover:border-pink-400 border-2"
>{{$view.Last}}</a>
{{end}}
</div>
{{end}}
{{end}}

View File

@ -27,6 +27,15 @@
</svg>{{i18n "AdminAllowListTitle"}}
</a>
<a
href="{{urlTo "admin:invites:overview"}}"
class="{{if current_page_is "admin:invites:overview"}}bg-gray-300 {{else}}hover:bg-gray-200 {{end}}pr-1 pl-2 py-3 sm:py-1 rounded-md flex flex-row items-center font-semibold text-sm text-gray-700 hover:text-gray-800 truncate"
>
<svg class="text-purple-600 w-4 h-4 mr-1" viewBox="0 0 24 24">
<path fill="currentColor" d="M15,4A4,4 0 0,0 11,8A4,4 0 0,0 15,12A4,4 0 0,0 19,8A4,4 0 0,0 15,4M15,5.9C16.16,5.9 17.1,6.84 17.1,8C17.1,9.16 16.16,10.1 15,10.1A2.1,2.1 0 0,1 12.9,8A2.1,2.1 0 0,1 15,5.9M4,7V10H1V12H4V15H6V12H9V10H6V7H4M15,13C12.33,13 7,14.33 7,17V20H23V17C23,14.33 17.67,13 15,13M15,14.9C17.97,14.9 21.1,16.36 21.1,17V18.1H8.9V17C8.9,16.36 12,14.9 15,14.9Z" />
</svg>{{i18n "NavAdminInvites"}}
</a>
<a
href="{{urlTo "complete:notice:list"}}"
class="{{if current_page_is "complete:notice:list"}}bg-gray-300 {{else}}hover:bg-gray-200 {{end}}pr-1 pl-2 py-3 sm:py-1 rounded-md flex flex-row items-center font-semibold text-sm text-gray-700 hover:text-gray-800 truncate"