invite form testing
This commit is contained in:
parent
bbcab73cb5
commit
c7bcef4339
|
@ -217,6 +217,7 @@ func runroomsrv() error {
|
|||
db.AuthWithSSB,
|
||||
db.AuthFallback,
|
||||
db.AllowList,
|
||||
db.Invites,
|
||||
db.Notices,
|
||||
db.PinnedNotices,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue