// 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) }