github.com/volatiletech/authboss@v2.4.1+incompatible/recover/recover_test.go (about)

     1  package recover
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha512"
     6  	"encoding/base64"
     7  	"errors"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"strings"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/volatiletech/authboss"
    15  	"github.com/volatiletech/authboss/mocks"
    16  )
    17  
    18  const (
    19  	testSelector = `rnaGE8TDilrINHPxq/2xNU1FUTzsUSX8FvN5YzooyyWKk88fw1DjjbKBRGFtGew9OeZ+xeCC4mslfvQQMYspIg==`
    20  	testVerifier = `W1Mz30QhavVM4d8jKaFtxGBfb4GX+fOn7V0Pc1WeftgtyOtY5OX7sY9gIeY5CIY4n8LvfWy14W7/6rs2KO9pgA==`
    21  	testToken    = `w5OZ51E61Q6wsJOVr9o7KmyepP7Od5VBHQ1ADDUBkiGGMjKfnMFPjtvNpLjLKJqffw72KWZzNLj0Cs8wqywdEQ==`
    22  )
    23  
    24  func TestInit(t *testing.T) {
    25  	t.Parallel()
    26  
    27  	ab := authboss.New()
    28  
    29  	router := &mocks.Router{}
    30  	renderer := &mocks.Renderer{}
    31  	mailRenderer := &mocks.Renderer{}
    32  	errHandler := &mocks.ErrorHandler{}
    33  	ab.Config.Core.Router = router
    34  	ab.Config.Core.ViewRenderer = renderer
    35  	ab.Config.Core.MailRenderer = mailRenderer
    36  	ab.Config.Core.ErrorHandler = errHandler
    37  
    38  	r := &Recover{}
    39  	if err := r.Init(ab); err != nil {
    40  		t.Fatal(err)
    41  	}
    42  
    43  	if err := renderer.HasLoadedViews(PageRecoverStart, PageRecoverEnd); err != nil {
    44  		t.Error(err)
    45  	}
    46  	if err := mailRenderer.HasLoadedViews(EmailRecoverHTML, EmailRecoverTxt); err != nil {
    47  		t.Error(err)
    48  	}
    49  
    50  	if err := router.HasGets("/recover", "/recover/end"); err != nil {
    51  		t.Error(err)
    52  	}
    53  	if err := router.HasPosts("/recover", "/recover/end"); err != nil {
    54  		t.Error(err)
    55  	}
    56  }
    57  
    58  type testHarness struct {
    59  	recover *Recover
    60  	ab      *authboss.Authboss
    61  
    62  	bodyReader *mocks.BodyReader
    63  	mailer     *mocks.Emailer
    64  	redirector *mocks.Redirector
    65  	renderer   *mocks.Renderer
    66  	responder  *mocks.Responder
    67  	session    *mocks.ClientStateRW
    68  	storer     *mocks.ServerStorer
    69  }
    70  
    71  func testSetup() *testHarness {
    72  	harness := &testHarness{}
    73  
    74  	harness.ab = authboss.New()
    75  	harness.bodyReader = &mocks.BodyReader{}
    76  	harness.mailer = &mocks.Emailer{}
    77  	harness.redirector = &mocks.Redirector{}
    78  	harness.renderer = &mocks.Renderer{}
    79  	harness.responder = &mocks.Responder{}
    80  	harness.session = mocks.NewClientRW()
    81  	harness.storer = mocks.NewServerStorer()
    82  
    83  	harness.ab.Paths.RecoverOK = "/recover/ok"
    84  	harness.ab.Modules.MailNoGoroutine = true
    85  
    86  	harness.ab.Config.Core.BodyReader = harness.bodyReader
    87  	harness.ab.Config.Core.Logger = mocks.Logger{}
    88  	harness.ab.Config.Core.Mailer = harness.mailer
    89  	harness.ab.Config.Core.Redirector = harness.redirector
    90  	harness.ab.Config.Core.MailRenderer = harness.renderer
    91  	harness.ab.Config.Core.Responder = harness.responder
    92  	harness.ab.Config.Storage.SessionState = harness.session
    93  	harness.ab.Config.Storage.Server = harness.storer
    94  
    95  	harness.recover = &Recover{harness.ab}
    96  
    97  	return harness
    98  }
    99  
   100  func TestStartGet(t *testing.T) {
   101  	t.Parallel()
   102  
   103  	h := testSetup()
   104  
   105  	r := mocks.Request("GET")
   106  	w := httptest.NewRecorder()
   107  
   108  	if err := h.recover.StartGet(w, r); err != nil {
   109  		t.Error(err)
   110  	}
   111  
   112  	if w.Code != http.StatusOK {
   113  		t.Error("code was wrong:", w.Code)
   114  	}
   115  	if h.responder.Page != PageRecoverStart {
   116  		t.Error("page was wrong:", h.responder.Page)
   117  	}
   118  	if h.responder.Data != nil {
   119  		t.Error("expected no data:", h.responder.Data)
   120  	}
   121  }
   122  
   123  func TestStartPostSuccess(t *testing.T) {
   124  	t.Parallel()
   125  
   126  	h := testSetup()
   127  
   128  	h.bodyReader.Return = &mocks.Values{
   129  		PID: "test@test.com",
   130  	}
   131  	h.storer.Users["test@test.com"] = &mocks.User{
   132  		Email:    "test@test.com",
   133  		Password: "i can't recall, doesn't seem like something bcrypted though",
   134  	}
   135  
   136  	r := mocks.Request("GET")
   137  	w := httptest.NewRecorder()
   138  
   139  	if err := h.recover.StartPost(w, r); err != nil {
   140  		t.Error(err)
   141  	}
   142  
   143  	if w.Code != http.StatusTemporaryRedirect {
   144  		t.Error("code was wrong:", w.Code)
   145  	}
   146  	if h.redirector.Options.RedirectPath != h.ab.Config.Paths.RecoverOK {
   147  		t.Error("page was wrong:", h.responder.Page)
   148  	}
   149  	if len(h.redirector.Options.Success) == 0 {
   150  		t.Error("expected a nice success message")
   151  	}
   152  
   153  	if h.mailer.Email.To[0] != "test@test.com" {
   154  		t.Error("e-mail to address is wrong:", h.mailer.Email.To)
   155  	}
   156  	if !strings.HasSuffix(h.mailer.Email.Subject, "Password Reset") {
   157  		t.Error("e-mail subject line is wrong:", h.mailer.Email.Subject)
   158  	}
   159  	if len(h.renderer.Data[DataRecoverURL].(string)) == 0 {
   160  		t.Errorf("the renderer's url in data was missing: %#v", h.renderer.Data)
   161  	}
   162  }
   163  
   164  func TestStartPostFailure(t *testing.T) {
   165  	t.Parallel()
   166  
   167  	h := testSetup()
   168  
   169  	h.bodyReader.Return = &mocks.Values{
   170  		PID: "test@test.com",
   171  	}
   172  
   173  	r := mocks.Request("GET")
   174  	w := httptest.NewRecorder()
   175  
   176  	if err := h.recover.StartPost(w, r); err != nil {
   177  		t.Error(err)
   178  	}
   179  
   180  	if w.Code != http.StatusTemporaryRedirect {
   181  		t.Error("code was wrong:", w.Code)
   182  	}
   183  	if h.redirector.Options.RedirectPath != h.ab.Config.Paths.RecoverOK {
   184  		t.Error("page was wrong:", h.responder.Page)
   185  	}
   186  	if len(h.redirector.Options.Success) == 0 {
   187  		t.Error("expected a nice success message")
   188  	}
   189  
   190  	if len(h.mailer.Email.To) != 0 {
   191  		t.Error("should not have sent an e-mail out!")
   192  	}
   193  }
   194  
   195  func TestEndGet(t *testing.T) {
   196  	t.Parallel()
   197  
   198  	h := testSetup()
   199  
   200  	h.bodyReader.Return = &mocks.Values{
   201  		Token: "abcd",
   202  	}
   203  
   204  	r := mocks.Request("GET")
   205  	w := httptest.NewRecorder()
   206  
   207  	if err := h.recover.EndGet(w, r); err != nil {
   208  		t.Error(err)
   209  	}
   210  
   211  	if w.Code != http.StatusOK {
   212  		t.Error("code was wrong:", w.Code)
   213  	}
   214  	if h.responder.Page != PageRecoverEnd {
   215  		t.Error("page was wrong:", h.responder.Page)
   216  	}
   217  	if h.responder.Data[DataRecoverToken].(string) != "abcd" {
   218  		t.Errorf("recovery token is wrong: %#v", h.responder.Data)
   219  	}
   220  }
   221  
   222  func TestEndPostSuccess(t *testing.T) {
   223  	t.Parallel()
   224  
   225  	h := testSetup()
   226  
   227  	h.bodyReader.Return = &mocks.Values{
   228  		Token: testToken,
   229  	}
   230  	h.storer.Users["test@test.com"] = &mocks.User{
   231  		Email:              "test@test.com",
   232  		Password:           "to-overwrite",
   233  		RecoverSelector:    testSelector,
   234  		RecoverVerifier:    testVerifier,
   235  		RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, 1),
   236  	}
   237  
   238  	r := mocks.Request("POST")
   239  	w := httptest.NewRecorder()
   240  
   241  	if err := h.recover.EndPost(w, r); err != nil {
   242  		t.Error(err)
   243  	}
   244  
   245  	if w.Code != http.StatusTemporaryRedirect {
   246  		t.Error("code was wrong:", w.Code)
   247  	}
   248  	if p := h.redirector.Options.RedirectPath; p != h.ab.Paths.RecoverOK {
   249  		t.Error("path was wrong:", p)
   250  	}
   251  	if len(h.session.ClientValues[authboss.SessionKey]) != 0 {
   252  		t.Error("should not have logged in the user")
   253  	}
   254  	if !strings.Contains(h.redirector.Options.Success, "updated password") {
   255  		t.Error("should talk about recovering the password")
   256  	}
   257  	if strings.Contains(h.redirector.Options.Success, "logged in") {
   258  		t.Error("should not talk about logging in")
   259  	}
   260  }
   261  
   262  func TestEndPostSuccessLogin(t *testing.T) {
   263  	t.Parallel()
   264  
   265  	h := testSetup()
   266  
   267  	h.ab.Config.Modules.RecoverLoginAfterRecovery = true
   268  	h.bodyReader.Return = &mocks.Values{
   269  		Token: testToken,
   270  	}
   271  	h.storer.Users["test@test.com"] = &mocks.User{
   272  		Email:              "test@test.com",
   273  		Password:           "to-overwrite",
   274  		RecoverSelector:    testSelector,
   275  		RecoverVerifier:    testVerifier,
   276  		RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, 1),
   277  	}
   278  
   279  	r := mocks.Request("POST")
   280  	w := httptest.NewRecorder()
   281  
   282  	if err := h.recover.EndPost(h.ab.NewResponse(w), r); err != nil {
   283  		t.Error(err)
   284  	}
   285  
   286  	if w.Code != http.StatusTemporaryRedirect {
   287  		t.Error("code was wrong:", w.Code)
   288  	}
   289  	if p := h.redirector.Options.RedirectPath; p != h.ab.Paths.RecoverOK {
   290  		t.Error("path was wrong:", p)
   291  	}
   292  	if len(h.session.ClientValues[authboss.SessionKey]) == 0 {
   293  		t.Error("it should have logged in the user")
   294  	}
   295  	if !strings.Contains(h.redirector.Options.Success, "logged in") {
   296  		t.Error("should talk about logging in")
   297  	}
   298  }
   299  
   300  func TestEndPostValidationFailure(t *testing.T) {
   301  	t.Parallel()
   302  
   303  	h := testSetup()
   304  
   305  	h.bodyReader.Return = &mocks.Values{
   306  		Errors: []error{errors.New("password is not sufficiently complex")},
   307  	}
   308  	h.storer.Users["test@test.com"] = &mocks.User{
   309  		Email:              "test@test.com",
   310  		Password:           "to-overwrite",
   311  		RecoverSelector:    testSelector,
   312  		RecoverVerifier:    testVerifier,
   313  		RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, 1),
   314  	}
   315  
   316  	r := mocks.Request("POST")
   317  	w := httptest.NewRecorder()
   318  
   319  	if err := h.recover.EndPost(w, r); err != nil {
   320  		t.Error(err)
   321  	}
   322  
   323  	if w.Code != http.StatusOK {
   324  		t.Error("code was wrong:", w.Code)
   325  	}
   326  	if h.responder.Page != PageRecoverEnd {
   327  		t.Error("rendered the wrong page")
   328  	}
   329  	if m, ok := h.responder.Data[authboss.DataValidation].(map[string][]string); !ok {
   330  		t.Error("expected validation errors")
   331  	} else if m[""][0] != "password is not sufficiently complex" {
   332  		t.Error("error message data was not correct:", m[""])
   333  	}
   334  	if len(h.session.ClientValues[authboss.SessionKey]) != 0 {
   335  		t.Error("should not have logged in the user")
   336  	}
   337  }
   338  
   339  func TestEndPostInvalidBase64(t *testing.T) {
   340  	t.Parallel()
   341  
   342  	h := testSetup()
   343  
   344  	h.bodyReader.Return = &mocks.Values{
   345  		Token: "a",
   346  	}
   347  
   348  	r := mocks.Request("GET")
   349  	w := httptest.NewRecorder()
   350  
   351  	if err := h.recover.EndPost(w, r); err != nil {
   352  		t.Error(err)
   353  	}
   354  
   355  	invalidCheck(t, h, w)
   356  }
   357  
   358  func TestEndPostExpiredToken(t *testing.T) {
   359  	t.Parallel()
   360  
   361  	h := testSetup()
   362  
   363  	h.bodyReader.Return = &mocks.Values{
   364  		Token: testToken,
   365  	}
   366  	h.storer.Users["test@test.com"] = &mocks.User{
   367  		Email:              "test@test.com",
   368  		Password:           "to-overwrite",
   369  		RecoverSelector:    testSelector,
   370  		RecoverVerifier:    testVerifier,
   371  		RecoverTokenExpiry: time.Now().UTC().AddDate(0, 0, -1),
   372  	}
   373  
   374  	r := mocks.Request("GET")
   375  	w := httptest.NewRecorder()
   376  
   377  	if err := h.recover.EndPost(w, r); err != nil {
   378  		t.Error(err)
   379  	}
   380  
   381  	invalidCheck(t, h, w)
   382  }
   383  
   384  func TestEndPostUserNotExist(t *testing.T) {
   385  	t.Parallel()
   386  
   387  	h := testSetup()
   388  
   389  	h.bodyReader.Return = &mocks.Values{
   390  		Token: testToken,
   391  	}
   392  
   393  	r := mocks.Request("GET")
   394  	w := httptest.NewRecorder()
   395  
   396  	if err := h.recover.EndPost(w, r); err != nil {
   397  		t.Error(err)
   398  	}
   399  
   400  	invalidCheck(t, h, w)
   401  }
   402  
   403  func TestMailURL(t *testing.T) {
   404  	t.Parallel()
   405  
   406  	h := testSetup()
   407  	h.ab.Config.Paths.RootURL = "https://api.test.com:6343"
   408  	h.ab.Config.Paths.Mount = "/v1/auth"
   409  
   410  	want := "https://api.test.com:6343/v1/auth/recover/end?token=abc"
   411  	if got := h.recover.mailURL("abc"); got != want {
   412  		t.Error("want:", want, "got:", got)
   413  	}
   414  
   415  	h.ab.Config.Mail.RootURL = "https://test.com:3333/testauth"
   416  
   417  	want = "https://test.com:3333/testauth/recover/end?token=abc"
   418  	if got := h.recover.mailURL("abc"); got != want {
   419  		t.Error("want:", want, "got:", got)
   420  	}
   421  }
   422  
   423  func invalidCheck(t *testing.T, h *testHarness, w *httptest.ResponseRecorder) {
   424  	t.Helper()
   425  
   426  	if w.Code != http.StatusOK {
   427  		t.Error("code was wrong:", w.Code)
   428  	}
   429  	if h.responder.Page != PageRecoverEnd {
   430  		t.Error("page was wrong:", h.responder.Page)
   431  	}
   432  	if h.responder.Data[authboss.DataValidation].(map[string][]string)[""][0] != "recovery token is invalid" {
   433  		t.Error("expected a vague error to mislead")
   434  	}
   435  }
   436  
   437  func TestGenerateRecoverCreds(t *testing.T) {
   438  	t.Parallel()
   439  
   440  	selector, verifier, token, err := GenerateRecoverCreds()
   441  	if err != nil {
   442  		t.Error(err)
   443  	}
   444  
   445  	if verifier == selector {
   446  		t.Error("the verifier and selector should be different")
   447  	}
   448  
   449  	// base64 length: n = 64; 4*(64/3) = 85.3; round to nearest 4: 88
   450  	if len(verifier) != 88 {
   451  		t.Errorf("verifier length was wrong (%d): %s", len(verifier), verifier)
   452  	}
   453  
   454  	// base64 length: n = 64; 4*(64/3) = 85.3; round to nearest 4: 88
   455  	if len(selector) != 88 {
   456  		t.Errorf("selector length was wrong (%d): %s", len(selector), selector)
   457  	}
   458  
   459  	// base64 length: n = 64; 4*(64/3) = 85.33; round to nearest 4: 88
   460  	if len(token) != 88 {
   461  		t.Errorf("token length was wrong (%d): %s", len(token), token)
   462  	}
   463  
   464  	rawToken, err := base64.URLEncoding.DecodeString(token)
   465  	if err != nil {
   466  		t.Error(err)
   467  	}
   468  
   469  	rawSelector, err := base64.StdEncoding.DecodeString(selector)
   470  	if err != nil {
   471  		t.Error(err)
   472  	}
   473  	rawVerifier, err := base64.StdEncoding.DecodeString(verifier)
   474  	if err != nil {
   475  		t.Error(err)
   476  	}
   477  
   478  	checkSelector := sha512.Sum512(rawToken[:recoverTokenSplit])
   479  	if 0 != bytes.Compare(checkSelector[:], rawSelector) {
   480  		t.Error("expected selector to match")
   481  	}
   482  	checkVerifier := sha512.Sum512(rawToken[recoverTokenSplit:])
   483  	if 0 != bytes.Compare(checkVerifier[:], rawVerifier) {
   484  		t.Error("expected verifier to match")
   485  	}
   486  }