1
0
mirror of https://github.com/volatiletech/authboss.git synced 2024-12-12 10:45:11 +02:00
authboss/recover/recover_test.go

534 lines
14 KiB
Go

package recover
import (
"bytes"
"fmt"
"html/template"
"log"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"time"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/mocks"
)
const (
testURLBase64Token = "MTIzNA=="
testStdBase64Token = "gdyb21LQTcIANtvYMT7QVQ=="
)
func testSetup() (r *Recover, s *mocks.MockStorer, l *bytes.Buffer) {
s = mocks.NewMockStorer()
l = &bytes.Buffer{}
ab := authboss.New()
ab.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`))
ab.LayoutHTMLEmail = template.Must(template.New("").Parse(`<strong>{{template "authboss" .}}</strong>`))
ab.LayoutTextEmail = template.Must(template.New("").Parse(`{{template "authboss" .}}`))
ab.Storer = s
ab.XSRFName = "xsrf"
ab.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string {
return "xsrfvalue"
}
ab.PrimaryID = authboss.StoreUsername
ab.LogWriter = l
ab.Policies = []authboss.Validator{
authboss.Rules{
FieldName: "username",
Required: true,
MinLength: 2,
MaxLength: 4,
AllowWhitespace: false,
},
authboss.Rules{
FieldName: "password",
Required: true,
MinLength: 4,
MaxLength: 8,
AllowWhitespace: false,
},
}
r = &Recover{}
if err := r.Initialize(ab); err != nil {
panic(err)
}
return r, s, l
}
func testRequest(ab *authboss.Authboss, method string, postFormValues ...string) (*authboss.Context, *httptest.ResponseRecorder, *http.Request, authboss.ClientStorerErr) {
r, err := http.NewRequest(method, "", nil)
if err != nil {
panic(err)
}
sessionStorer := mocks.NewMockClientStorer()
ctx := mocks.MockRequestContext(ab, postFormValues...)
ctx.SessionStorer = sessionStorer
return ctx, httptest.NewRecorder(), r, sessionStorer
}
func TestRecover(t *testing.T) {
t.Parallel()
r, _, _ := testSetup()
storage := r.Storage()
if storage[r.PrimaryID] != authboss.String {
t.Error("Expected storage KV:", r.PrimaryID, authboss.String)
}
if storage[authboss.StoreEmail] != authboss.String {
t.Error("Expected storage KV:", authboss.StoreEmail, authboss.String)
}
if storage[authboss.StorePassword] != authboss.String {
t.Error("Expected storage KV:", authboss.StorePassword, authboss.String)
}
if storage[StoreRecoverToken] != authboss.String {
t.Error("Expected storage KV:", StoreRecoverToken, authboss.String)
}
if storage[StoreRecoverTokenExpiry] != authboss.String {
t.Error("Expected storage KV:", StoreRecoverTokenExpiry, authboss.String)
}
routes := r.Routes()
if routes["/recover"] == nil {
t.Error("Expected route '/recover' with handleFunc")
}
if routes["/recover/complete"] == nil {
t.Error("Expected route '/recover/complete' with handleFunc")
}
}
func TestRecover_startHandlerFunc_GET(t *testing.T) {
t.Parallel()
rec, _, _ := testSetup()
ctx, w, r, _ := testRequest(rec.Authboss, "GET")
if err := rec.startHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected status:", w.Code)
}
body := w.Body.String()
if !strings.Contains(body, `<form action="recover"`) {
t.Error("Should have rendered a form")
}
if !strings.Contains(body, `name="`+rec.PrimaryID) {
t.Error("Form should contain the primary ID field")
}
if !strings.Contains(body, `name="confirm_`+rec.PrimaryID) {
t.Error("Form should contain the confirm primary ID field")
}
}
func TestRecover_startHandlerFunc_POST_ValidationFails(t *testing.T) {
t.Parallel()
rec, _, _ := testSetup()
ctx, w, r, _ := testRequest(rec.Authboss, "POST")
if err := rec.startHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected status:", w.Code)
}
if !strings.Contains(w.Body.String(), "Cannot be blank") {
t.Error("Expected error about email being blank")
}
}
func TestRecover_startHandlerFunc_POST_UserNotFound(t *testing.T) {
t.Parallel()
rec, _, _ := testSetup()
ctx, w, r, _ := testRequest(rec.Authboss, "POST", "username", "john", "confirm_username", "john")
err := rec.startHandlerFunc(ctx, w, r)
if err == nil {
t.Error("Expected error:", err)
}
rerr, ok := err.(authboss.ErrAndRedirect)
if !ok {
t.Error("Expected ErrAndRedirect error")
}
if rerr.Location != rec.RecoverOKPath {
t.Error("Unexpected location:", rerr.Location)
}
if rerr.FlashSuccess != recoverInitiateSuccessFlash {
t.Error("Unexpected success flash", rerr.FlashSuccess)
}
}
func TestRecover_startHandlerFunc_POST(t *testing.T) {
t.Parallel()
rec, storer, _ := testSetup()
storer.Users["john"] = authboss.Attributes{authboss.StoreUsername: "john", authboss.StoreEmail: "a@b.c"}
sentEmail := false
goRecoverEmail = func(_ *Recover, _, _ string) {
sentEmail = true
}
ctx, w, r, sessionStorer := testRequest(rec.Authboss, "POST", "username", "john", "confirm_username", "john")
if err := rec.startHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if !sentEmail {
t.Error("Expected email to have been sent")
}
if val, err := storer.Users["john"].StringErr(StoreRecoverToken); err != nil {
t.Error("Unexpected error:", err)
} else if len(val) <= 0 {
t.Error("Unexpected Recover Token to be set")
}
if val, err := storer.Users["john"].DateTimeErr(StoreRecoverTokenExpiry); err != nil {
t.Error("Unexpected error:", err)
} else if !val.After(time.Now()) {
t.Error("Expected recovery token expiry to be greater than now")
}
if w.Code != http.StatusFound {
t.Error("Unexpected status:", w.Code)
}
loc := w.Header().Get("Location")
if loc != rec.RecoverOKPath {
t.Error("Unexpected location:", loc)
}
if value, ok := sessionStorer.Get(authboss.FlashSuccessKey); !ok {
t.Error("Expected success flash message")
} else if value != recoverInitiateSuccessFlash {
t.Error("Unexpected success flash message")
}
}
func TestRecover_startHandlerFunc_OtherMethods(t *testing.T) {
t.Parallel()
rec, _, _ := testSetup()
methods := []string{"HEAD", "PUT", "DELETE", "TRACE", "CONNECT"}
for i, method := range methods {
_, w, r, _ := testRequest(rec.Authboss, method)
if err := rec.startHandlerFunc(nil, w, r); err != nil {
t.Errorf("%d> Unexpected error: %s", i, err)
}
if http.StatusMethodNotAllowed != w.Code {
t.Errorf("%d> Expected status code %d, got %d", i, http.StatusMethodNotAllowed, w.Code)
continue
}
}
}
func TestRecover_newToken(t *testing.T) {
t.Parallel()
regexURL := regexp.MustCompile(`^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2}==|[A-Za-z0-9-_]{3}=)?$`)
regexSTD := regexp.MustCompile(`^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$`)
encodedToken, encodedSum, _ := newToken()
if !regexURL.MatchString(encodedToken) {
t.Error("Expected encodedToken to be base64 encoded")
}
if !regexSTD.MatchString(encodedSum) {
t.Error("Expected encodedSum to be base64 encoded")
}
}
func TestRecover_sendRecoverMail_FailToSend(t *testing.T) {
t.Parallel()
r, _, logger := testSetup()
mailer := mocks.NewMockMailer()
mailer.SendErr = "failed to send"
r.Mailer = mailer
r.sendRecoverEmail("", "")
if !strings.Contains(logger.String(), "failed to send") {
t.Error("Expected logged to have msg:", "failed to send")
}
}
func TestRecover_sendRecoverEmail(t *testing.T) {
t.Parallel()
r, _, _ := testSetup()
mailer := mocks.NewMockMailer()
r.EmailSubjectPrefix = "foo "
r.RootURL = "bar"
r.Mailer = mailer
r.sendRecoverEmail("a@b.c", "abc=")
if len(mailer.Last.To) != 1 {
t.Error("Expected 1 to email")
}
if mailer.Last.To[0] != "a@b.c" {
t.Error("Unexpected to email:", mailer.Last.To[0])
}
if mailer.Last.Subject != "foo Password Reset" {
t.Error("Unexpected subject:", mailer.Last.Subject)
}
url := fmt.Sprintf("%s/recover/complete?token=abc=", r.RootURL)
if !strings.Contains(mailer.Last.HTMLBody, url) {
t.Error("Expected HTMLBody to contain url:", url)
}
if !strings.Contains(mailer.Last.TextBody, url) {
t.Error("Expected TextBody to contain url:", url)
}
}
func TestRecover_completeHandlerFunc_GET_VerifyFails(t *testing.T) {
t.Parallel()
rec, storer, _ := testSetup()
ctx, w, r, _ := testRequest(rec.Authboss, "GET", "token", testURLBase64Token)
err := rec.completeHandlerFunc(ctx, w, r)
rerr, ok := err.(authboss.ErrAndRedirect)
if !ok {
t.Error("Expected ErrAndRedirect")
}
if rerr.Location != "/" {
t.Error("Unexpected location:", rerr.Location)
}
var zeroTime time.Time
storer.Users["john"] = authboss.Attributes{StoreRecoverToken: testStdBase64Token, StoreRecoverTokenExpiry: zeroTime}
ctx, w, r, _ = testRequest(rec.Authboss, "GET", "token", testURLBase64Token)
err = rec.completeHandlerFunc(ctx, w, r)
rerr, ok = err.(authboss.ErrAndRedirect)
if !ok {
t.Error("Expected ErrAndRedirect")
}
if rerr.Location != "/recover" {
t.Error("Unexpected location:", rerr.Location)
}
if rerr.FlashError != recoverTokenExpiredFlash {
t.Error("Unexpcted flash error:", rerr.FlashError)
}
}
func TestRecover_completeHandlerFunc_GET(t *testing.T) {
t.Parallel()
rec, storer, _ := testSetup()
storer.Users["john"] = authboss.Attributes{StoreRecoverToken: testStdBase64Token, StoreRecoverTokenExpiry: time.Now().Add(1 * time.Hour)}
ctx, w, r, _ := testRequest(rec.Authboss, "GET", "token", testURLBase64Token)
if err := rec.completeHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected status:", w.Code)
}
body := w.Body.String()
if !strings.Contains(body, `<form action="recover/complete"`) {
t.Error("Should have rendered a form")
}
if !strings.Contains(body, `name="password"`) {
t.Error("Form should contain the password field")
}
if !strings.Contains(body, `name="confirm_password"`) {
t.Error("Form should contain the confirm password field")
}
if !strings.Contains(body, `name="token"`) {
t.Error("Form should contain the token field")
}
}
func TestRecover_completeHandlerFunc_POST_TokenMissing(t *testing.T) {
t.Parallel()
rec, _, _ := testSetup()
ctx, w, r, _ := testRequest(rec.Authboss, "POST")
err := rec.completeHandlerFunc(ctx, w, r)
if err.Error() != "Failed to retrieve client attribute: token" {
t.Error("Unexpected error:", err)
}
}
func TestRecover_completeHandlerFunc_POST_ValidationFails(t *testing.T) {
t.Parallel()
rec, _, _ := testSetup()
ctx, w, r, _ := testRequest(rec.Authboss, "POST", "token", testURLBase64Token)
if err := rec.completeHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected status:", w.Code)
}
if !strings.Contains(w.Body.String(), "Cannot be blank") {
t.Error("Expected error about password being blank")
}
}
func TestRecover_completeHandlerFunc_POST_VerificationFails(t *testing.T) {
t.Parallel()
rec, _, _ := testSetup()
ctx, w, r, _ := testRequest(rec.Authboss, "POST", "token", testURLBase64Token, authboss.StorePassword, "abcd", "confirm_"+authboss.StorePassword, "abcd")
if err := rec.completeHandlerFunc(ctx, w, r); err == nil {
log.Println(w.Body.String())
t.Error("Expected error")
}
}
func TestRecover_completeHandlerFunc_POST(t *testing.T) {
t.Parallel()
rec, storer, _ := testSetup()
storer.Users["john"] = authboss.Attributes{rec.PrimaryID: "john", StoreRecoverToken: testStdBase64Token, StoreRecoverTokenExpiry: time.Now().Add(1 * time.Hour), authboss.StorePassword: "asdf"}
cbCalled := false
rec.Callbacks = authboss.NewCallbacks()
rec.Callbacks.After(authboss.EventPasswordReset, func(_ *authboss.Context) error {
cbCalled = true
return nil
})
ctx, w, r, sessionStorer := testRequest(rec.Authboss, "POST", "token", testURLBase64Token, authboss.StorePassword, "abcd", "confirm_"+authboss.StorePassword, "abcd")
if err := rec.completeHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
var zeroTime time.Time
u := storer.Users["john"]
if password, ok := u.String(authboss.StorePassword); !ok || password == "asdf" {
t.Error("Expected password to have been reset")
}
if recToken, ok := u.String(StoreRecoverToken); !ok || recToken != "" {
t.Error("Expected recovery token to have been zeroed")
}
if reCExpiry, ok := u.DateTime(StoreRecoverTokenExpiry); !ok || !reCExpiry.Equal(zeroTime) {
t.Error("Expected recovery token expiry to have been zeroed")
}
if !cbCalled {
t.Error("Expected EventPasswordReset callback to have been fired")
}
if val, ok := sessionStorer.Get(authboss.SessionKey); !ok || val != "john" {
t.Error("Expected SessionKey to be:", "john")
}
if w.Code != http.StatusFound {
t.Error("Unexpected status:", w.Code)
}
loc := w.Header().Get("Location")
if loc != rec.AuthLogoutOKPath {
t.Error("Unexpected location:", loc)
}
}
func Test_verifyToken_MissingToken(t *testing.T) {
t.Parallel()
testSetup()
ctx := &authboss.Context{}
if _, err := verifyToken(ctx); err == nil {
t.Error("Expected error about missing token")
}
}
func Test_verifyToken_InvalidToken(t *testing.T) {
t.Parallel()
rec, storer, _ := testSetup()
storer.Users["a"] = authboss.Attributes{
StoreRecoverToken: testStdBase64Token,
}
ctx := mocks.MockRequestContext(rec.Authboss, "token", "asdf")
if _, err := verifyToken(ctx); err != authboss.ErrUserNotFound {
t.Error("Unexpected error:", err)
}
}
func Test_verifyToken_ExpiredToken(t *testing.T) {
t.Parallel()
rec, storer, _ := testSetup()
storer.Users["a"] = authboss.Attributes{
StoreRecoverToken: testStdBase64Token,
StoreRecoverTokenExpiry: time.Now().Add(time.Duration(-24) * time.Hour),
}
ctx := mocks.MockRequestContext(rec.Authboss, "token", testURLBase64Token)
if _, err := verifyToken(ctx); err != errRecoveryTokenExpired {
t.Error("Unexpected error:", err)
}
}
func Test_verifyToken(t *testing.T) {
t.Parallel()
rec, storer, _ := testSetup()
storer.Users["a"] = authboss.Attributes{
StoreRecoverToken: testStdBase64Token,
StoreRecoverTokenExpiry: time.Now().Add(time.Duration(24) * time.Hour),
}
ctx := mocks.MockRequestContext(rec.Authboss, "token", testURLBase64Token)
attrs, err := verifyToken(ctx)
if err != nil {
t.Error("Unexpected error:", err)
}
if attrs == nil {
t.Error("Unexpected nil attrs")
}
}