go-ssb-room/web/handlers/invites_test.go

385 lines
11 KiB
Go

// SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021
//
// SPDX-License-Identifier: MIT
package handlers
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
refs "github.com/ssbc/go-ssb-refs"
"github.com/ssbc/go-ssb-room/v2/roomdb"
weberrors "github.com/ssbc/go-ssb-room/v2/web/errors"
"github.com/ssbc/go-ssb-room/v2/web/router"
"github.com/ssbc/go-ssb-room/v2/web/webassert"
)
func TestInviteShowAcceptForm(t *testing.T) {
ts := setup(t)
t.Run("token doesnt exist", func(t *testing.T) {
a, r := assert.New(t), require.New(t)
testToken := "nonexistant-test-token"
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
doc, resp := ts.Client.GetHTML(acceptURL404)
// 500 until https://github.com/ssbc/go-ssb-room/issues/66 is fixed
a.Equal(http.StatusInternalServerError, resp.Code)
// check database calls
r.EqualValues(1, ts.InvitesDB.GetByTokenCallCount())
_, tokenFromArg := ts.InvitesDB.GetByTokenArgsForCall(0)
a.Equal(testToken, tokenFromArg)
// fix #66
// assertLocalized(t, doc, []localizedElement{
// {"#welcome", "AuthFallbackWelcome"},
// {"title", "AuthFallbackTitle"},
// })
gotErr := doc.Find("#errBody").Text()
wantErr := weberrors.ErrNotFound{What: "invite"}
a.EqualError(wantErr, gotErr)
})
t.Run("token DOES exist", func(t *testing.T) {
a, r := assert.New(t), require.New(t)
testToken := "existing-test-token"
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
doc, resp := ts.Client.GetHTML(validAcceptURL)
a.Equal(http.StatusOK, resp.Code)
// check database calls
r.EqualValues(2, ts.InvitesDB.GetByTokenCallCount())
_, tokenFromArg := ts.InvitesDB.GetByTokenArgsForCall(1)
a.Equal(testToken, tokenFromArg)
webassert.Localized(t, doc, []webassert.LocalizedElement{
{"#claim-invite-uri", "InviteFacadeJoin"},
{"title", "InviteFacadeTitle"},
})
// Fallback URL in data-href-fallback
fallbackURL := ts.URLTo(router.CompleteInviteFacadeFallback, "token", testToken)
joinDataHrefFallback, ok := doc.Find("#claim-invite-uri").Attr("data-href-fallback")
a.Equal(fallbackURL.String(), joinDataHrefFallback)
a.True(ok)
// ssb-uri in href
joinDataHref, ok := doc.Find("#claim-invite-uri").Attr("href")
a.True(ok)
joinURI, err := url.Parse(joinDataHref)
r.NoError(err)
a.Equal("ssb", joinURI.Scheme)
a.Equal("experimental", joinURI.Opaque)
params := joinURI.Query()
a.Equal("claim-http-invite", params.Get("action"))
inviteParam := params.Get("invite")
a.Equal(testToken, inviteParam)
postTo := params.Get("postTo")
expectedConsumeInviteURL := ts.URLTo(router.CompleteInviteConsume)
a.Equal(expectedConsumeInviteURL.String(), postTo)
})
}
func TestInviteShowAcceptFormOnAndroidChrome(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
testToken := "existing-test-token"
validAcceptURL := ts.URLTo(router.CompleteInviteFacade, "token", testToken)
// Mimic Android Chrome
var uaHeader = make(http.Header)
uaHeader.Set("User-Agent", "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5 Build/MOB30H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.133 Mobile Safari/537.36")
ts.Client.SetHeaders(uaHeader)
// prep the mocked db for http:200
fakeExistingInvite := roomdb.Invite{ID: 1234}
ts.InvitesDB.GetByTokenReturns(fakeExistingInvite, nil)
// request the form
doc, resp := ts.Client.GetHTML(validAcceptURL)
a.Equal(http.StatusOK, resp.Code)
// check database calls
r.EqualValues(1, ts.InvitesDB.GetByTokenCallCount())
_, tokenFromArg := ts.InvitesDB.GetByTokenArgsForCall(0)
a.Equal(testToken, tokenFromArg)
webassert.Localized(t, doc, []webassert.LocalizedElement{
{"#claim-invite-uri", "InviteFacadeJoin"},
{"title", "InviteFacadeTitle"},
})
// ssb-uri in href
joinDataHref, ok := doc.Find("#claim-invite-uri").Attr("href")
a.True(ok)
joinURI, err := url.Parse(joinDataHref)
r.NoError(err)
a.Equal("intent", joinURI.Scheme)
a.Equal("experimental", joinURI.Host)
params := joinURI.Query()
a.Equal("claim-http-invite", params.Get("action"))
inviteParam := params.Get("invite")
a.Equal(testToken, inviteParam)
postTo := params.Get("postTo")
expectedConsumeInviteURL := ts.URLTo(router.CompleteInviteConsume)
a.Equal(expectedConsumeInviteURL.String(), postTo)
frag := joinURI.Fragment
a.Equal("Intent;scheme=ssb;end;", frag)
}
func TestInviteConsumeInviteHTTP(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
testToken := "existing-test-token-2"
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)
doc, resp := ts.Client.GetHTML(validAcceptURL)
a.Equal(http.StatusOK, resp.Code)
form := doc.Find("form#inviteConsume")
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.String(), consumeInviteURLString)
webassert.CSRFTokenPresent(t, form)
webassert.ElementsInForm(t, form, []webassert.FormElement{
{Name: "invite", Type: "hidden", Value: testToken},
{Name: "id", Type: "text"},
})
// get the corresponding token from the page
csrfTokenElem := form.Find(`input[name="gorilla.csrf.Token"]`)
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")
// create the consume request
testNewMember, err := refs.NewFeedRefFromBytes(bytes.Repeat([]byte{1}, 32), refs.RefAlgoFeedSSB1)
if err != nil {
t.Error(err)
}
consumeVals := url.Values{
"invite": []string{testToken},
"id": []string{testNewMember.String()},
csrfName: []string{csrfValue},
}
// construct the consume endpoint url
consumeInviteURL := ts.URLTo(router.CompleteInviteConsume)
// construct the header with the Referer or csrf check
var csrfCookieHeader = http.Header(map[string][]string{})
csrfCookieHeader.Set("Referer", "https://localhost")
ts.Client.SetHeaders(csrfCookieHeader)
// prepare the mock
ts.InvitesDB.ConsumeReturns(testInvite, nil)
// send the POST
resp = ts.Client.PostForm(consumeInviteURL, consumeVals)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for sign in")
// check how consume was called
r.EqualValues(1, ts.InvitesDB.ConsumeCallCount())
_, tokenFromArg, newMemberRef := ts.InvitesDB.ConsumeArgsForCall(0)
a.Equal(testToken, tokenFromArg)
a.True(newMemberRef.Equal(testNewMember))
}
func TestInviteConsumeInviteJSON(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
testToken := "existing-test-token-2"
testInvite := roomdb.Invite{ID: 4321}
ts.InvitesDB.GetByTokenReturns(testInvite, nil)
// check if the token is still valid
checkInviteURL := ts.URLTo(router.CompleteInviteFacade)
qvals := url.Values{
"token": []string{testToken},
"encoding": []string{"json"},
}
checkInviteURL.RawQuery = qvals.Encode()
// send the request and check the json
resp := ts.Client.GetBody(checkInviteURL)
result := resp.Result()
a.Equal(http.StatusOK, result.StatusCode)
var reply struct {
Invite string
PostTo string
}
err := json.NewDecoder(result.Body).Decode(&reply)
r.NoError(err)
// construct the consume endpoint url
consumeInviteURL := ts.URLTo(router.CompleteInviteConsume)
a.Equal(consumeInviteURL.String(), reply.PostTo, "wrong postTo in JSON body")
a.Equal(testToken, reply.Invite, "wrong invite token")
// create the consume request
testNewMember, err := refs.NewFeedRefFromBytes(bytes.Repeat([]byte{1}, 32), refs.RefAlgoFeedSSB1)
if err != nil {
t.Error(err)
}
var consume inviteConsumePayload
consume.Invite = testToken
consume.ID = testNewMember
// prepare the mock
ts.InvitesDB.ConsumeReturns(testInvite, nil)
// send the POST
resp = ts.Client.SendJSON(consumeInviteURL, consume)
a.Equal(http.StatusOK, resp.Code, "wrong HTTP status code for sign in")
// check how consume was called
r.EqualValues(1, ts.InvitesDB.ConsumeCallCount())
_, tokenFromArg, newMemberRef := ts.InvitesDB.ConsumeArgsForCall(0)
a.Equal(testToken, tokenFromArg)
a.True(newMemberRef.Equal(testNewMember))
var jsonConsumeResp inviteConsumeJSONResponse
err = json.NewDecoder(resp.Body).Decode(&jsonConsumeResp)
r.NoError(err)
a.Equal("successful", jsonConsumeResp.Status)
gotRA := jsonConsumeResp.RoomAddress
a.True(strings.HasPrefix(gotRA, "net:localhost:8008~shs:"), "not for the test host: %s", gotRA)
a.True(strings.HasSuffix(gotRA, base64.StdEncoding.EncodeToString(ts.NetworkInfo.RoomID.PubKey())), "public key missing? %s", gotRA)
}
func TestInviteConsumptionDenied(t *testing.T) {
ts := setup(t)
a, r := assert.New(t), require.New(t)
testToken := "existing-test-token-2"
validAcceptURL := ts.URLTo(router.CompleteInviteFacade, "token", testToken)
r.NotNil(validAcceptURL)
testInvite := roomdb.Invite{ID: 4321}
ts.InvitesDB.GetByTokenReturns(testInvite, nil)
ts.DeniedKeysDB.HasFeedReturns(true)
// create the consume request
testNewMember, err := refs.NewFeedRefFromBytes(bytes.Repeat([]byte{1}, 32), refs.RefAlgoFeedSSB1)
if err != nil {
t.Error(err)
}
var consume inviteConsumePayload
consume.Invite = testToken
consume.ID = testNewMember
// construct the consume endpoint url
consumeInviteURL := ts.URLTo(router.CompleteInviteConsume)
r.NotNil(consumeInviteURL)
// prepare the mock
ts.InvitesDB.ConsumeReturns(testInvite, nil)
// send the POST
resp := ts.Client.SendJSON(consumeInviteURL, consume)
// decode the json response
var jsonConsumeResp inviteConsumeJSONResponse
err = json.NewDecoder(resp.Body).Decode(&jsonConsumeResp)
r.NoError(err)
// json response should indicate an error for the denied key
a.Equal("error", jsonConsumeResp.Status)
// invite should not be consumed
r.EqualValues(0, ts.InvitesDB.ConsumeCallCount())
}
func TestOpenModeCreateInviteHTML(t *testing.T) {
ts := setup(t)
r := require.New(t)
someToken := "fake-token"
ts.InvitesDB.CreateReturns(someToken, nil)
doc, resp := ts.Client.GetHTML(ts.URLTo(router.OpenModeCreateInvite))
r.Equal(http.StatusOK, resp.Code)
facadeLink := doc.Find("#invite-facade-link")
r.NotNil(facadeLink)
r.Contains(facadeLink.AttrOr("href", ""), someToken)
r.Contains(facadeLink.Text(), someToken)
}
func TestOpenModeCreateInviteJSON(t *testing.T) {
ts := setup(t)
r := require.New(t)
someToken := "fake-token"
ts.InvitesDB.CreateReturns(someToken, nil)
req, err := http.NewRequest("POST", ts.URLTo(router.OpenModeCreateInvite).String(), nil)
r.NoError(err)
req.Header.Set("Accept", "application/json")
recorder := httptest.NewRecorder()
ts.Mux.ServeHTTP(recorder, req)
r.Equal(http.StatusOK, recorder.Code)
response := map[string]string{}
err = json.Unmarshal(recorder.Body.Bytes(), &response)
r.NoError(err)
require.Contains(t, response["url"], someToken)
}