Merge pull request #64 from ssb-ngi-pointer/44-notices

Add tests for notices view
This commit is contained in:
Alexander Cobleigh 2021-03-16 14:59:09 +01:00 committed by GitHub
commit 04ba3b9477
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 386 additions and 23 deletions

5
.gitignore vendored
View File

@ -1,3 +1,6 @@
# vim
.*.sw[a-z]
# the binaries
cmd/server/server
cmd/insert-user/insert-user
@ -11,4 +14,4 @@ roomdb/sqlite/testrun
# build artifacts from node.js project web/styles
node_modules
web/assets/styles/compiled.*
web/assets/style.css
web/assets/style.css

View File

@ -19,6 +19,9 @@ import (
// AuthFallbackService might be helpful for scenarios where one lost access to his ssb device or key
type AuthFallbackService interface {
// Check receives the username and password (in clear) and checks them accordingly.
// If it's a valid combination it returns the user ID, or an error if they are not.
auth.Auther
Create(ctx context.Context, user string, password []byte) (int64, error)
@ -98,7 +101,7 @@ type InviteService interface {
// PinnedNoticesService allows an admin to assign Notices to specific placeholder pages.
// like updates, privacy policy, code of conduct
type PinnedNoticesService interface {
// List returns a list of all the pinned notices with their corrosponding notices and languges
// List returns a list of all the pinned notices with their corresponding notices and languages
List(context.Context) (PinnedNotices, error)
// Set assigns a fixed page name to an page ID and a language to allow for multiple translated versions of the same page.

View File

@ -24,6 +24,8 @@ type AuthFallback struct {
db *sql.DB
}
// Check receives the username and password (in clear) and checks them accordingly.
// If it's a valid combination it returns the user ID, or an error if they are not.
func (af AuthFallback) Check(name, password string) (interface{}, error) {
ctx := context.Background()
found, err := models.AuthFallbacks(qm.Where("name = ?", name)).One(ctx, af.db)

View File

@ -103,9 +103,9 @@ func TestPinnedNotices(t *testing.T) {
}
for i, tcase := range cases {
desc, has := allTheNotices[tcase.Name]
notices, has := allTheNotices[tcase.Name]
r.True(has, "case %d failed - notice %s not in map", i, tcase.Name)
r.Len(desc, tcase.Count, "case %d failed - wrong number of notices for %s", i, tcase.Name)
r.Len(notices, tcase.Count, "case %d failed - wrong number of notices for %s", i, tcase.Name)
}
})
@ -130,7 +130,6 @@ func TestPinnedNotices(t *testing.T) {
notice.Title = "política de privacidad"
notice.Content = "solo una prueba"
notice.Language = "es"
// save the new notice
err = db.Notices.Save(ctx, &notice)
r.NoError(err)

View File

@ -91,7 +91,7 @@ func TestAliasesRevokeConfirmation(t *testing.T) {
a.Equal(addURL.String(), action)
webassert.InputsInForm(t, form, []webassert.InputElement{
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "name", Type: "hidden", Value: testEntry.Name},
})
}

View File

@ -61,7 +61,7 @@ func TestAllowListAdd(t *testing.T) {
a.Equal(addURL.String(), action)
webassert.InputsInForm(t, formSelection, []webassert.InputElement{
webassert.ElementsInForm(t, formSelection, []webassert.FormElement{
{Name: "pub_key", Type: "text"},
})
@ -181,7 +181,7 @@ func TestAllowListRemoveConfirmation(t *testing.T) {
a.Equal(addURL.String(), action)
webassert.InputsInForm(t, form, []webassert.InputElement{
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "id", Type: "hidden", Value: "666"},
})
}

View File

@ -78,7 +78,8 @@ func newSession(t *testing.T) *testSession {
testFuncs["current_page_is"] = func(routeName string) bool {
return true
}
testFuncs["is_logged_in"] = func() *roomdb.User { return nil }
testFuncs["is_logged_in"] = func() *roomdb.User { return ts.User }
testFuncs["urlToNotice"] = func(name string) string { return "" }
r, err := render.New(web.Templates,

View File

@ -44,7 +44,7 @@ func TestInvitesCreateForm(t *testing.T) {
a.Equal(addURL.String(), action)
webassert.InputsInForm(t, formSelection, []webassert.InputElement{
webassert.ElementsInForm(t, formSelection, []webassert.FormElement{
{Name: "alias_suggestion", Type: "text"},
})
}

View File

@ -45,6 +45,12 @@ func (h noticeHandler) addTranslation(rw http.ResponseWriter, req *http.Request)
return
}
// reply with 405 error: Method not allowed
if req.Method != "POST" {
err := weberrors.ErrBadRequest{Where: "http method type", Details: fmt.Errorf("add translation only accepts POST requests, sorry!")}
h.r.Error(rw, req, http.StatusMethodNotAllowed, err)
}
pinnedName := roomdb.PinnedNoticeName(req.FormValue("name"))
if !pinnedName.Valid() {
err := weberrors.ErrBadRequest{Where: "name", Details: fmt.Errorf("invalid pinned notice name")}
@ -54,6 +60,11 @@ func (h noticeHandler) addTranslation(rw http.ResponseWriter, req *http.Request)
var n roomdb.Notice
n.Title = req.FormValue("title")
if n.Title == "" {
err = weberrors.ErrBadRequest{Where: "title", Details: fmt.Errorf("title can't be empty")}
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
}
// TODO: validate languages properly
n.Language = req.FormValue("language")
@ -64,6 +75,11 @@ func (h noticeHandler) addTranslation(rw http.ResponseWriter, req *http.Request)
}
n.Content = req.FormValue("content")
if n.Content == "" {
err = weberrors.ErrBadRequest{Where: "content", Details: fmt.Errorf("content can't be empty")}
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
}
// https://github.com/russross/blackfriday/issues/575
n.Content = strings.Replace(n.Content, "\r\n", "\n", -1)
@ -137,6 +153,11 @@ func (h noticeHandler) save(rw http.ResponseWriter, req *http.Request) {
}
n.Title = req.FormValue("title")
if n.Title == "" {
err = weberrors.ErrBadRequest{Where: "title", Details: fmt.Errorf("title can't be empty")}
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
}
// TODO: validate languages properly
n.Language = req.FormValue("language")
@ -147,6 +168,11 @@ func (h noticeHandler) save(rw http.ResponseWriter, req *http.Request) {
}
n.Content = req.FormValue("content")
if n.Content == "" {
err = weberrors.ErrBadRequest{Where: "content", Details: fmt.Errorf("content can't be empty")}
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
}
// https://github.com/russross/blackfriday/issues/575
n.Content = strings.Replace(n.Content, "\r\n", "\n", -1)

View File

@ -0,0 +1,153 @@
package admin
import (
"fmt"
"net/http"
"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/ssb-ngi-pointer/go-ssb-room/web/webassert"
"github.com/stretchr/testify/assert"
)
// Verifies that the notice.go save handler is like, actually, called.
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"}
content := []string{"Absolutely Thrilling Content"}
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)
formValues := url.Values{"id": id, "title": title, "content": content, "language": language}
resp := ts.Client.PostForm(u.String(), formValues)
a.Equal(http.StatusSeeOther, resp.Code, "POST should work")
a.Equal(1, ts.NoticeDB.SaveCallCount(), "noticedb should have saved after POST completed")
}
// Verifies that the notices.go:save handler refuses requests missing required parameters
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"}
title := []string{"SSB Breaking News: This Test Is Great"}
content := []string{"Absolutely Thrilling Content"}
language := []string{"pt"}
/* save without id */
u := urlTo(router.AdminNoticeSave)
emptyParams := url.Values{}
resp := ts.Client.PostForm(u.String(), 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)
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)
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)
a.Equal(http.StatusInternalServerError, resp.Code, "saving without language should not work")
a.Equal(0, ts.NoticeDB.SaveCallCount(), "noticedb should never save incomplete requests")
}
// Verifies that /translation/add only accepts POST requests
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)
// verify that a GET request is no bueno
u := urlTo(router.AdminNoticeAddTranslation, "name", roomdb.NoticeNews.String())
_, resp := ts.Client.GetHTML(u.String())
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:
id := []string{"1"}
title := []string{"Bom Dia! SSB Breaking News: This Test Is Great"}
content := []string{"conteúdo muito bom"}
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)
a.Equal(http.StatusTemporaryRedirect, resp.Code)
}
// Verifies that the "add a translation" page contains all the required form fields (id/title/content/language)
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)
// to test translations we first need to add a notice to the notice mockdb
notice := roomdb.Notice{
ID: 1,
Title: "News",
Content: "Breaking News: This Room Has News",
Language: "en-GB",
}
// 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())
form := html.Find("form")
a.Equal(http.StatusOK, resp.Code, "Wrong HTTP status code")
// FormElement defaults to input if tag omitted
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "title"},
{Name: "language"},
{Tag: "textarea", Name: "content"},
})
}
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)
// Create mock notice data to operate on
notice := roomdb.Notice{
ID: 1,
Title: "News",
Content: "Breaking News: This Room Has News",
Language: "en-GB",
}
ts.NoticeDB.GetByIDReturns(notice, nil)
u := urlTo(router.AdminNoticeEdit, "id", 1)
html, resp := ts.Client.GetHTML(u.String())
form := html.Find("form")
a.Equal(http.StatusOK, resp.Code, "Wrong HTTP status code")
// check for all the form elements & verify their initial contents are set correctly
// FormElement defaults to input if tag omitted
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "title", Value: notice.Title},
{Name: "language", Value: notice.Language},
{Name: "id", Value: fmt.Sprintf("%d", notice.ID), Type: "hidden"},
{Tag: "textarea", Name: "content"},
})
}

View File

@ -96,7 +96,7 @@ func TestInviteShowAcceptForm(t *testing.T) {
webassert.CSRFTokenPresent(t, form)
webassert.InputsInForm(t, form, []webassert.InputElement{
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "token", Type: "hidden", Value: testToken},
{Name: "alias", Type: "text", Value: fakeExistingInvite.AliasSuggestion},
{Name: "new_member", Type: "text", Placeholder: wantNewMemberPlaceholder},
@ -133,7 +133,7 @@ func TestInviteShowAcceptForm(t *testing.T) {
r.Equal(1, form.Length())
webassert.CSRFTokenPresent(t, form)
webassert.InputsInForm(t, form, []webassert.InputElement{
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "token", Type: "hidden", Value: testToken},
{Name: "alias", Type: "text", Placeholder: "you@this.room"},
{Name: "new_member", Type: "text", Placeholder: wantNewMemberPlaceholder},

View File

@ -0,0 +1,169 @@
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
func TestNoticeSmokeTest(t *testing.T) {
ts := setup(t)
a := assert.New(t)
noticeData := roomdb.Notice{
ID: 1,
Title: "Welcome!",
}
ts.NoticeDB.GetByIDReturns(noticeData, nil)
html, res := ts.Client.GetHTML("/notice/show?id=1")
a.Equal(http.StatusOK, res.Code, "wrong HTTP status code")
a.Equal("Welcome!", html.Find("title").Text())
}
func TestNoticeMarkdownServedCorrectly(t *testing.T) {
ts := setup(t)
a := assert.New(t)
markdown := `
Hello world!
## The loveliest of rooms is here
`
noticeData := roomdb.Notice{
ID: 1,
Title: "Welcome!",
Content: markdown,
}
ts.NoticeDB.GetByIDReturns(noticeData, nil)
html, res := ts.Client.GetHTML("/notice/show?id=1")
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())
}
// First we get the notices page (to see the buttons are NOT there)
// 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)
noticeData := roomdb.Notice{
ID: 42,
Title: "Welcome!",
Content: `super simple conent`,
}
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"
editButtonSelector := `#edit-notice`
doc, resp := ts.Client.GetHTML(noticeURL.String())
a.Equal(http.StatusOK, resp.Code)
// empty selection <=> we have no link
a.EqualValues(0, doc.Find(editButtonSelector).Length())
// 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.AuthFallbackSignInForm)
r.NotNil(formEndpoint)
formEndpoint.Host = "localhost"
formEndpoint.Scheme = "https"
doc, resp = ts.Client.GetHTML(formEndpoint.String())
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)
csrfTokenElem := doc.Find("input[type=hidden]")
a.Equal(1, csrfTokenElem.Length())
csrfName, has := csrfTokenElem.Attr("name")
a.True(has, "should have a name attribute")
csrfValue, has := csrfTokenElem.Attr("value")
a.True(has, "should have value attribute")
loginVals := url.Values{
"user": []string{"test"},
"pass": []string{"test"},
csrfName: []string{csrfValue},
}
// have the database return okay for any user
testUser := &roomdb.User{
ID: 23,
Name: "test admin",
}
ts.AuthFallbackDB.CheckReturns(testUser.ID, nil)
ts.AuthFallbackDB.GetByIDReturns(testUser, nil)
postEndpoint, err := ts.Router.Get(router.AuthFallbackSignIn).URL()
r.Nil(err)
postEndpoint.Host = "localhost"
postEndpoint.Scheme = "https"
// 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)
resp = ts.Client.PostForm(postEndpoint.String(), 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)
// now we are logged in, anchor tag should be there
doc, resp = ts.Client.GetHTML(noticeURL.String())
a.Equal(http.StatusOK, resp.Code)
a.EqualValues(1, doc.Find(editButtonSelector).Length())
}

View File

@ -11,6 +11,7 @@
<div class="h-10"></div>
{{if is_logged_in}}
<a
id="edit-notice"
href="{{urlTo "admin:notice:edit" "id" .ID}}"
class="self-start shadow rounded px-4 h-8 flex flex-row justify-center items-center text-gray-100 bg-pink-600 hover:bg-pink-700 focus:outline-none focus:ring-2 focus:ring-pink-600 focus:ring-opacity-50"
>{{i18n "NoticeEditTitle"}}</a>

View File

@ -40,7 +40,9 @@ func NewURLTo(appRouter *mux.Router) func(string, ...interface{}) *url.URL {
return func(routeName string, ps ...interface{}) *url.URL {
route := appRouter.Get(routeName)
if route == nil {
level.Warn(l).Log("msg", "no such route", "route", routeName, "params", fmt.Sprintf("%v", ps))
// TODO: https://github.com/ssb-ngi-pointer/go-ssb-room/issues/35 for a
// for reference, see https://github.com/ssb-ngi-pointer/go-ssb-room/pull/64
// level.Warn(l).Log("msg", "no such route", "route", routeName, "params", fmt.Sprintf("%v", ps))
return &url.URL{}
}

View File

@ -31,36 +31,40 @@ func CSRFTokenPresent(t *testing.T, sel *goquery.Selection) {
a.Equal("hidden", tipe, "wrong type on csrf field")
}
type InputElement struct {
Name, Value, Type, Placeholder string
type FormElement struct {
Tag, Name, Value, Type, Placeholder string
}
// InputsInForm checks a list of defined elements. It tries to find them by input[name=$name]
// and then proceeds with asserting their value, type or placeholder (if the fields in InputElement are not "")
func InputsInForm(t *testing.T, form *goquery.Selection, elems []InputElement) {
// ElementsInForm checks a list of defined elements. It tries to find them by input[name=$name]
// and then proceeds with asserting their value, type or placeholder (if the fields in FormElement are not "")
func ElementsInForm(t *testing.T, form *goquery.Selection, elems []FormElement) {
a := assert.New(t)
for _, e := range elems {
// empty Tag defaults to <input>
if e.Tag == "" {
e.Tag = "input"
}
inputSelector := form.Find(fmt.Sprintf("input[name=%s]", e.Name))
ok := a.Equal(1, inputSelector.Length(), "expected to find input with name %s", e.Name)
elementSelector := form.Find(fmt.Sprintf("%s[name=%s]", e.Tag, e.Name))
ok := a.Equal(1, elementSelector.Length(), "expected to find element with name %s", e.Name)
if !ok {
continue
}
if e.Value != "" {
value, has := inputSelector.Attr("value")
value, has := elementSelector.Attr("value")
a.True(has, "expected value attribute input[name=%s]", e.Name)
a.Equal(e.Value, value, "wrong value attribute on input[name=%s]", e.Name)
}
if e.Type != "" {
tipe, has := inputSelector.Attr("type")
tipe, has := elementSelector.Attr("type")
a.True(has, "expected type attribute input[name=%s]", e.Name)
a.Equal(e.Type, tipe, "wrong type attribute on input[name=%s]", e.Name)
}
if e.Placeholder != "" {
tipe, has := inputSelector.Attr("placeholder")
tipe, has := elementSelector.Attr("placeholder")
a.True(has, "expected placeholder attribute input[name=%s]", e.Name)
a.Equal(e.Placeholder, tipe, "wrong placeholder attribute on input[name=%s]", e.Name)
}