1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-01-18 04:58:57 +02:00
authboss/recover/recover_test.go
2015-02-08 23:12:29 -08:00

630 lines
15 KiB
Go

package recover
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"html/template"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"os"
"testing"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/mocks"
"gopkg.in/authboss.v0/internal/views"
)
type failStorer int
func (_ failStorer) Create(_ string, _ authboss.Attributes) error { return nil }
func (_ failStorer) Put(_ string, _ authboss.Attributes) error { return nil }
func (_ failStorer) Get(_ string, _ authboss.AttributeMeta) (interface{}, error) { return nil, nil }
func Test_Initialize(t *testing.T) {
t.Parallel()
config := &authboss.Config{ViewsPath: os.TempDir()}
m := &RecoverModule{}
if err := m.Initialize(config); err == nil {
t.Error("Expected error")
} else if err.Error() != "recover: Need a RecoverStorer." {
t.Error("Got error but wrong reason:", err)
}
config.Storer = new(failStorer)
if err := m.Initialize(config); err == nil {
t.Error("Expected error")
} else if err.Error() != "recover: RecoverStorer required for recover functionality." {
t.Error("Got error but wrong reason:", err)
}
config.Storer = mocks.NewMockStorer()
if err := m.Initialize(config); err == nil {
t.Error("Expected error")
} else if err.Error() != "recover: Layout required for Recover functionallity." {
t.Error("Got error but wrong reason:", err)
}
var err error
config.Layout, err = template.New("").Parse(`{{template "authboss" .}}`)
if err != nil {
t.Fatal("Unexpected error:", err)
}
if err := m.Initialize(config); err == nil {
t.Error("Expected error:", err)
} else if err.Error() != "recover: LayoutEmail required for Recover functionallity." {
t.Error("Got error but wrong reason:", err)
}
config.LayoutEmail, err = template.New("").Parse(`{{template "authboss" .}}`)
if err != nil {
t.Fatal("Unexpected error:", err)
}
if err := m.Initialize(config); err != nil {
t.Error("Unexpected error:", err)
}
}
func testValidTestConfig() *authboss.Config {
config := &authboss.Config{}
config.Storer = mocks.NewMockStorer()
config.EmailFrom = "auth@boss.com"
var err error
if config.Layout, err = views.AssetToTemplate("layout.tpl"); err != nil {
panic(err)
}
if config.LayoutEmail, err = views.AssetToTemplate("layoutEmail.tpl"); err != nil {
panic(err)
}
config.RecoverRedirect = "/login"
config.RecoverInitiateSuccessFlash = "sf"
config.RecoverTokenExpiredFlash = "exf"
config.RecoverFailedErrorFlash = "errf"
config.Policies = []authboss.Validator{
authboss.Rules{
FieldName: "username",
Required: true,
},
}
config.ConfirmFields = []string{"username", "confirmUsername"}
config.LogWriter = &bytes.Buffer{}
config.Mailer = &mocks.MockMailer{}
config.EmailFrom = "auth@boss.com"
config.HostName = "localhost"
return config
}
func testValidRecoverModule() (*RecoverModule, *bytes.Buffer) {
c := testValidTestConfig()
m := &RecoverModule{}
if err := m.Initialize(c); err != nil {
panic(err)
}
return m, c.LogWriter.(*bytes.Buffer)
}
func Test_Routes(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
tests := []struct {
Attr string
Kind authboss.DataType
}{
{attrUsername, authboss.String},
{attrRecoverToken, authboss.String},
{attrEmail, authboss.String},
{attrRecoverTokenExpiry, authboss.String},
{attrPassword, authboss.String},
}
options := m.Storage()
for i, test := range tests {
if kind, ok := options[test.Attr]; !ok {
t.Errorf("%s> Expected attr: %s", i, test.Attr)
} else if kind != test.Kind {
t.Errorf("%s> Expected DataType: %s", i, test.Kind)
}
}
}
func Test_Storage(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
routes := m.Routes()
if _, ok := routes["recover"]; !ok {
t.Error("Expected route: recover")
}
if _, ok := routes["recover/complete"]; !ok {
t.Error("Expected route: recover/complete")
}
}
func testHttpRequest(method, url string, data url.Values) (*httptest.ResponseRecorder, *http.Request, *authboss.Context) {
var body io.Reader
if method != "GET" {
body = strings.NewReader(data.Encode())
}
r, err := http.NewRequest(method, url, body)
if err != nil {
panic(err)
}
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
ctx, err := authboss.ContextFromRequest(r)
if err != nil {
panic(err)
}
ctx.SessionStorer = mocks.MockClientStorer{}
return w, r, ctx
}
func Test_recoverHandlerFunc_GET(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
w, r, ctx := testHttpRequest("GET", "/login", nil)
m.recoverHandlerFunc(ctx, w, r)
expectedBody := &bytes.Buffer{}
if err := m.templates[tplRecover].Execute(expectedBody, pageRecover{}); err != nil {
panic(err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected code:", w.Code)
}
if !bytes.Equal(expectedBody.Bytes(), w.Body.Bytes()) {
t.Error("Unexpected body:", w.Body.String())
}
}
func Test_recoverHandlerFunc_POST_RecoveryFailed(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
w, r, ctx := testHttpRequest("POST", "/login", url.Values{"username": []string{"a"}, "confirmUsername": []string{"a"}})
tpl := m.templates[tplRecover]
expectedBody := &bytes.Buffer{}
if err := tpl.Execute(expectedBody, pageRecover{
Username: "a",
ConfirmUsername: "a",
FlashError: m.config.RecoverFailedErrorFlash,
}); err != nil {
panic(err)
}
// missing storer will cause this to fail
m.recoverHandlerFunc(ctx, w, r)
if w.Code != http.StatusOK {
t.Error("Unexpected code:", w.Code)
}
if !bytes.Equal(expectedBody.Bytes(), w.Body.Bytes()) {
t.Error("Unexpected body:", w.Body.String())
}
}
func Test_recoverHandlerFunc_POST(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
w, r, ctx := testHttpRequest("POST", "/login", url.Values{"username": []string{"a"}, "confirmUsername": []string{"a"}})
storer, ok := m.config.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{"username": "", "password": "", "email": "a@b.c"}
m.recoverHandlerFunc(ctx, w, r)
if w.Code != http.StatusFound {
t.Error("Unexpected code:", w.Code)
}
location := w.Header().Get("Location")
if location != "/login" {
t.Error("Unexpected redirect:", location)
}
successFlash := ctx.SessionStorer.(mocks.MockClientStorer)[authboss.FlashSuccessKey]
if successFlash != m.config.RecoverInitiateSuccessFlash {
t.Error("Unexpected success flash message:", successFlash)
}
}
func Test_execTpl_TemplateExectionFail(t *testing.T) {
t.Parallel()
m, logger := testValidRecoverModule()
w := httptest.NewRecorder()
failTpl, err := template.New("").Parse("{{.Fail}}")
if err != nil {
panic("Failed to build tpl")
}
m.templates["fail.tpl"] = failTpl
m.execTpl("fail.tpl", w, pageRecover{})
if w.Code != http.StatusInternalServerError {
t.Error("Unexpected code:", w.Code)
}
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [unable to execute template]:")) {
t.Error("Expected log message starting with:", "recover [unable to execute template]:")
}
}
func Test_execTpl(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
w := httptest.NewRecorder()
page := pageRecover{"bobby", "bob", nil, "", m.config.RecoverFailedErrorFlash}
m.execTpl(tplRecover, w, page)
tpl := m.templates[tplRecover]
expectedBody := &bytes.Buffer{}
if err := tpl.Execute(expectedBody, page); err != nil {
panic(err)
}
if w.Code != http.StatusOK {
t.Error("Unexpected code:", w.Code)
}
if !bytes.Equal(expectedBody.Bytes(), w.Body.Bytes()) {
t.Error("Unexpected body:", w.Body.String())
}
}
func Test_recover_UsernameValidationFail(t *testing.T) {
t.Parallel()
m, logger := testValidRecoverModule()
ctx := mocks.MockRequestContext()
page, emailSent := m.recover(ctx)
if len(page.ErrMap["username"]) != 1 {
t.Error("Exepted single validation error for username")
}
if page.ErrMap["username"][0] != "Cannot be blank" {
t.Error("Unexpected validation error for username:", page.ErrMap["username"][0])
}
expectedLog := []byte("recover [validation failed]: username: Cannot be blank\n")
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Equal(expectedLog, actualLog) {
t.Errorf("Unexpected logs: %q", string(actualLog))
}
if emailSent != nil {
t.Error("Unexpected sent email")
}
}
func Test_recover_ConfirmUsernameCheckFail(t *testing.T) {
t.Parallel()
m, logger := testValidRecoverModule()
ctx := mocks.MockRequestContext("username", "a", "confirmUsername", "b")
page, emailSent := m.recover(ctx)
if len(page.ErrMap["username"]) != 0 {
t.Error("Exepted no validation errors for username")
}
if len(page.ErrMap["confirmUsername"]) != 1 {
t.Error("Exepted single validation error for confirmUsername")
}
if page.ErrMap["confirmUsername"][0] != "Does not match username" {
t.Error("Unexpected validation error for confirmUsername:", page.ErrMap["confirmUsername"][0])
}
expectedLog := []byte("recover [validation failed]: confirmUsername: Does not match username\n")
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Equal(expectedLog, actualLog) {
t.Error("Unexpected logs:", string(actualLog))
}
if emailSent != nil {
t.Error("Unexpected sent email")
}
}
func Test_recover_InvalidUser(t *testing.T) {
t.Parallel()
m, logger := testValidRecoverModule()
ctx := mocks.MockRequestContext("username", "a", "confirmUsername", "a")
page, emailSent := m.recover(ctx)
if page.ErrMap != nil {
t.Error("Exepted no validation errors")
}
if page.FlashError != m.config.RecoverFailedErrorFlash {
t.Error("Expected flash error:", m.config.RecoverFailedErrorFlash)
}
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [failed to recover]:")) {
t.Error("Expected log message starting with:", "recover [failed to recover]:")
}
if emailSent != nil {
t.Error("Unexpected sent email")
}
}
func Test_recover(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
storer, ok := m.config.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{"username": "", "password": "", "email": "a@b.c"}
ctx := mocks.MockRequestContext("username", "a", "confirmUsername", "a")
page, emailSent := m.recover(ctx)
if page != nil {
t.Error("Expected nil page")
}
if emailSent == nil {
t.Error("Expected sent email")
}
}
func Test_makeAndSendToken_MissingStorer(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
ctx := mocks.MockRequestContext()
err, ch := m.makeAndSendToken(ctx, "a")
if err == nil || err.Error() != authboss.ErrUserNotFound.Error() {
t.Error("Expected error:", authboss.ErrUserNotFound)
}
if ch != nil {
t.Error("Expected nil channel")
}
}
func Test_makeAndSendToken_CheckEmail(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
ctx := mocks.MockRequestContext()
storer, ok := m.config.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{"username": "", "password": ""}
// missing
err, ch := m.makeAndSendToken(ctx, "a")
expectedErr := fmt.Sprintf("email required: %v", attrEmail)
if err == nil || err.Error() != expectedErr {
t.Error("Expected error:", expectedErr)
}
if ch != nil {
t.Error("Expected nil channel")
}
// empty
storer.Users["a"] = authboss.Attributes{"username": "", "password": "", "email": ""}
err, ch = m.makeAndSendToken(ctx, "a")
if err == nil || err.Error() != expectedErr {
t.Error("Expected error:", expectedErr)
}
if ch != nil {
t.Error("Expected nil channel")
}
}
func Test_makeAndSendToken(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
ctx := mocks.MockRequestContext()
storer, ok := m.config.Storer.(*mocks.MockStorer)
if !ok {
panic("Failed to get storer")
}
storer.Users["a"] = authboss.Attributes{"username": "a", "password": "b", "email": "a@b.c"}
err, ch := m.makeAndSendToken(ctx, "a")
_, ok = storer.Users["a"][attrRecoverToken]
if !ok {
t.Error("Expected recover token")
}
_, ok = storer.Users["a"][attrRecoverTokenExpiry]
if !ok {
t.Error("Expected recover token expiry")
}
if err != nil {
t.Error("Unexpected error:", err)
}
if ch == nil {
t.Error("Unexpected nil channel")
}
}
func Test_sendRecoverEmail_InvalidTemplates(t *testing.T) {
t.Parallel()
m, logger := testValidRecoverModule()
failTpl, err := template.New("").Parse("{{.Fail}}")
if err != nil {
panic("Failed to build tpl")
}
// broken html template
originalHtmlEmail := m.emailTemplates[tplInitHTMLEmail]
m.emailTemplates[tplInitHTMLEmail] = failTpl
<-m.sendRecoverEmail("a@b.c", []byte("abc123"))
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [failed to build html email]:")) {
t.Error("Expected log message starting with:", "recover [failed to build html email]:")
}
// broken plain text template
m.emailTemplates[tplInitHTMLEmail] = originalHtmlEmail
m.emailTemplates[tplInitTextEmail] = failTpl
<-m.sendRecoverEmail("a@b.c", []byte("abc123"))
actualLog, err = ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [failed to build plaintext email]:")) {
t.Error("Expected log message starting with:", "recover [failed to build plaintext email]:")
}
}
type failMailer struct{}
func (_ failMailer) Send(_ authboss.Email) error {
return errors.New("")
}
func Test_sendRecoverEmail_FailToSend(t *testing.T) {
t.Parallel()
m, logger := testValidRecoverModule()
m.config.Mailer = failMailer{}
<-m.sendRecoverEmail("a@b.c", []byte("abc123"))
actualLog, err := ioutil.ReadAll(logger)
if err != nil {
panic(err)
}
if !bytes.Contains(actualLog, []byte("recover [failed to send email]:")) {
t.Error("Expecte log message starting with:", "recover [failed to send email]:")
}
}
func Test_sendRecoverEmail(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
<-m.sendRecoverEmail("a@b.c", []byte("abc123"))
mailer, ok := m.config.Mailer.(*mocks.MockMailer)
if !ok {
panic("Failed to assert mock mailer")
}
sent := mailer.Last
if len(sent.To) != 1 {
t.Error("Expected one to email")
}
if sent.To[0] != "a@b.c" {
t.Error("Unexpected to email:", sent.To[0])
}
if sent.From != m.config.EmailFrom {
t.Error("Unexpected from email:", sent.From)
}
if sent.Subject != "Password Reset" {
t.Error("Unexpected subject:", sent.Subject)
}
data := struct {
Link string
}{
fmt.Sprintf("%s/recover/complete?token=%s",
m.config.HostName,
base64.URLEncoding.EncodeToString([]byte("abc123")),
),
}
html, err := m.emailTemplates.ExecuteTemplate(tplInitHTMLEmail, data)
if err != nil {
panic(err)
}
test, err := m.emailTemplates.ExecuteTemplate(tplInitTextEmail, data)
if err != nil {
panic(err)
}
if !bytes.Equal(html.Bytes(), []byte(sent.HTMLBody)) {
t.Error("Unexpected html body:", sent.HTMLBody)
}
if !bytes.Equal(test.Bytes(), []byte(sent.TextBody)) {
t.Error("Unexpected text body:", sent.TextBody)
}
}
func Test_recoverHandlerFunc_OtherMethods(t *testing.T) {
t.Parallel()
m, _ := testValidRecoverModule()
for i, method := range []string{"HEAD", "PUT", "DELETE", "TRACE", "CONNECT"} {
w, r, _ := testHttpRequest(method, "/login", nil)
m.recoverHandlerFunc(nil, w, r)
if http.StatusMethodNotAllowed != w.Code {
t.Errorf("%d> Expected status code %d, got %d", i, http.StatusMethodNotAllowed, w.Code)
continue
}
}
}