1
0
mirror of https://github.com/volatiletech/authboss.git synced 2025-02-09 13:47:09 +02:00

Add new auth testing and tempaltes

This commit is contained in:
Kris Runzer 2015-02-24 15:01:56 -08:00
parent f716720759
commit 0928720a3f
7 changed files with 397 additions and 10 deletions

View File

@ -31,6 +31,14 @@ func (a *Auth) Initialize() (err error) {
return errors.New("auth: Need a Storer.")
}
if len(authboss.Cfg.XSRFName) == 0 {
return errors.New("auth: XSRFName must be set")
}
if authboss.Cfg.XSRFMaker == nil {
return errors.New("auth: XSRFMaker must be defined")
}
a.templates, err = render.LoadTemplates(authboss.Cfg.Layout, authboss.Cfg.ViewsPath, tplLogin)
if err != nil {
return err
@ -59,6 +67,7 @@ func (a *Auth) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r
if _, ok := ctx.SessionStorer.Get(authboss.SessionKey); ok {
if halfAuthed, ok := ctx.SessionStorer.Get(authboss.SessionHalfAuthKey); !ok || halfAuthed == "false" {
http.Redirect(w, r, authboss.Cfg.AuthLoginSuccessRoute, http.StatusFound)
return nil
}
}
@ -81,7 +90,7 @@ func (a *Auth) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r
case authboss.InterruptAccountNotConfirmed:
reason = "Your account has not been confirmed."
}
render.Redirect(ctx, w, r, "/", "", reason)
render.Redirect(ctx, w, r, "/login", "", reason)
return nil
}
@ -98,12 +107,10 @@ func (a *Auth) loginHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r
policies := authboss.FilterValidators(authboss.Cfg.Policies, authboss.Cfg.PrimaryID, authboss.StorePassword)
if validationErrs := ctx.Validate(policies); len(validationErrs) > 0 {
fmt.Fprintln(authboss.Cfg.LogWriter, "auth: form validation failed:", validationErrs.Map())
return a.templates.Render(ctx, w, r, tplLogin, errData)
}
if err := validateCredentials(ctx, key, password); err != nil {
fmt.Fprintln(authboss.Cfg.LogWriter, "auth: failed to validate credentials:", err)
return a.templates.Render(ctx, w, r, tplLogin, errData)
}

View File

@ -1 +1,356 @@
package auth
import (
"errors"
"html/template"
"net/http"
"net/http/httptest"
"strings"
"testing"
"gopkg.in/authboss.v0"
"gopkg.in/authboss.v0/internal/mocks"
)
func testSetup() (a *Auth, s *mocks.MockStorer) {
s = mocks.NewMockStorer()
authboss.Cfg = authboss.NewConfig()
authboss.Cfg.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`))
authboss.Cfg.Storer = s
authboss.Cfg.XSRFName = "xsrf"
authboss.Cfg.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string {
return "xsrfvalue"
}
authboss.Cfg.PrimaryID = authboss.StoreUsername
a = &Auth{}
if err := a.Initialize(); err != nil {
panic(err)
}
return a, s
}
func testRequest(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(postFormValues...)
ctx.SessionStorer = sessionStorer
return ctx, httptest.NewRecorder(), r, sessionStorer
}
func TestAuth(t *testing.T) {
a, _ := testSetup()
if err := a.Initialize(); err != nil {
t.Error("Unexcpeted error:", err)
}
storage := a.Storage()
if storage[authboss.Cfg.PrimaryID] != authboss.String {
t.Error("Expected storage KV:", authboss.Cfg.PrimaryID, authboss.String)
}
if storage[authboss.StorePassword] != authboss.String {
t.Error("Expected storage KV:", authboss.StorePassword, authboss.String)
}
routes := a.Routes()
if routes["login"] == nil {
t.Error("Expected route 'login' with handleFunc")
}
if routes["logout"] == nil {
t.Error("Expected route 'logout' with handleFunc")
}
}
func TestAuth_loginHandlerFunc_GET_RedirectsWhenHalfAuthed(t *testing.T) {
a, _ := testSetup()
ctx, w, r, sessionStore := testRequest("GET")
sessionStore.Put(authboss.SessionKey, "a")
sessionStore.Put(authboss.SessionHalfAuthKey, "false")
authboss.Cfg.AuthLoginSuccessRoute = "/dashboard"
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpeced error:", err)
}
if w.Code != http.StatusFound {
t.Error("Unexpcted status:", w.Code)
}
loc := w.Header().Get("Location")
if loc != authboss.Cfg.AuthLoginSuccessRoute {
t.Error("Unexpected redirect:", loc)
}
}
func TestAuth_loginHandlerFunc_GET(t *testing.T) {
a, _ := testSetup()
ctx, w, r, _ := testRequest("GET")
if err := a.loginHandlerFunc(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") {
t.Error("Should have rendered a form")
}
if !strings.Contains(body, `name="`+authboss.Cfg.PrimaryID) {
t.Error("Form should contain the primary ID field:", body)
}
if !strings.Contains(body, `name="password"`) {
t.Error("Form should contain password field:", body)
}
}
func TestAuth_loginHandlerFunc_POST_ReturnsErrorOnCallbackFailure(t *testing.T) {
a, _ := testSetup()
authboss.Cfg.Callbacks = authboss.NewCallbacks()
authboss.Cfg.Callbacks.Before(authboss.EventAuth, func(_ *authboss.Context) (authboss.Interrupt, error) {
return authboss.InterruptNone, errors.New("explode")
})
ctx, w, r, _ := testRequest("POST")
if err := a.loginHandlerFunc(ctx, w, r); err.Error() != "explode" {
t.Error("Unexpected error:", err)
}
}
func TestAuth_loginHandlerFunc_POST_RedirectsWhenInterrupted(t *testing.T) {
a, _ := testSetup()
authboss.Cfg.Callbacks = authboss.NewCallbacks()
authboss.Cfg.Callbacks.Before(authboss.EventAuth, func(_ *authboss.Context) (authboss.Interrupt, error) {
return authboss.InterruptAccountLocked, nil
})
ctx, w, r, sessionStore := testRequest("POST")
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusFound {
t.Error("Unexpected status:", w.Code)
}
loc := w.Header().Get("Location")
if loc != "/login" {
t.Error("Unexpeced location:", loc)
}
expectedMsg := "Your account has been locked."
if msg, ok := sessionStore.Get(authboss.FlashErrorKey); !ok || msg != expectedMsg {
t.Error("Expected error flash message:", expectedMsg)
}
authboss.Cfg.Callbacks = authboss.NewCallbacks()
authboss.Cfg.Callbacks.Before(authboss.EventAuth, func(_ *authboss.Context) (authboss.Interrupt, error) {
return authboss.InterruptAccountNotConfirmed, nil
})
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if w.Code != http.StatusFound {
t.Error("Unexpected status:", w.Code)
}
loc = w.Header().Get("Location")
if loc != "/login" {
t.Error("Unexpeced location:", loc)
}
expectedMsg = "Your account has not been confirmed."
if msg, ok := sessionStore.Get(authboss.FlashErrorKey); !ok || msg != expectedMsg {
t.Error("Expected error flash message:", expectedMsg)
}
}
func TestAuth_loginHandlerFunc_POST_AuthenticationFailure(t *testing.T) {
a, _ := testSetup()
ctx, w, r, _ := testRequest("POST", "username", "john", "password", "1")
if err := a.loginHandlerFunc(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, "invalid username and/or password") {
t.Error("Should have rendered with error")
}
ctx, w, r, _ = testRequest("POST", "username", "john", "password", "1234")
if err := a.loginHandlerFunc(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, "invalid username and/or password") {
t.Error("Should have rendered with error")
}
}
func TestAuth_loginHandlerFunc_POST(t *testing.T) {
a, storer := testSetup()
storer.Users["john"] = authboss.Attributes{"password": "$2a$10$B7aydtqVF9V8RSNx3lCKB.l09jqLV/aMiVqQHajtL7sWGhCS9jlOu"}
ctx, w, r, _ := testRequest("POST", "username", "john", "password", "1234")
cb := mocks.NewMockAfterCallback()
authboss.Cfg.Callbacks = authboss.NewCallbacks()
authboss.Cfg.Callbacks.After(authboss.EventAuth, cb.Fn)
authboss.Cfg.AuthLoginSuccessRoute = "/dashboard"
if err := a.loginHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if !cb.HasBeenCalled {
t.Error("Expected after callback to have been called")
}
if w.Code != http.StatusFound {
t.Error("Unexpected status:", w.Code)
}
loc := w.Header().Get("Location")
if loc != authboss.Cfg.AuthLoginSuccessRoute {
t.Error("Unexpeced location:", loc)
}
}
func TestAuth_loginHandlerFunc_OtherMethods(t *testing.T) {
a, _ := testSetup()
methods := []string{"HEAD", "PUT", "DELETE", "TRACE", "CONNECT"}
for i, method := range methods {
r, err := http.NewRequest(method, "/login", nil)
if err != nil {
t.Errorf("%d> Unexpected error '%s'", i, err)
}
w := httptest.NewRecorder()
if err := a.loginHandlerFunc(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 TestAuth_validateCredentials(t *testing.T) {
authboss.Cfg = authboss.NewConfig()
storer := mocks.NewMockStorer()
storer.GetErr = "Failed to load user"
authboss.Cfg.Storer = storer
ctx := authboss.Context{}
if err := validateCredentials(&ctx, "", ""); err.Error() != "Failed to load user" {
t.Error("Unexpected error:", err)
}
storer.GetErr = ""
storer.Users["john"] = authboss.Attributes{"password": "$2a$10$pgFsuQwdhwOdZp/v52dvHeEi53ZaI7dGmtwK4bAzGGN5A4nT6doqm"}
if err := validateCredentials(&ctx, "john", "b"); err == nil {
t.Error("Expected error about passwords mismatch")
}
sessions := mocks.NewMockClientStorer()
ctx.SessionStorer = sessions
if err := validateCredentials(&ctx, "john", "a"); err != nil {
t.Error("Unexpected error:", err)
}
val, ok := sessions.Values[authboss.SessionKey]
if !ok {
t.Error("Expected session to be set")
} else if val != "john" {
t.Error("Expected session value to be authed username")
}
}
func TestAuth_logoutHandlerFunc_GET(t *testing.T) {
a, _ := testSetup()
authboss.Cfg.AuthLogoutRoute = "/dashboard"
ctx, w, r, sessionStorer := testRequest("GET")
sessionStorer.Put(authboss.SessionKey, "asdf")
if err := a.logoutHandlerFunc(ctx, w, r); err != nil {
t.Error("Unexpected error:", err)
}
if _, ok := sessionStorer.Get(authboss.SessionKey); ok {
t.Errorf("Expected to be logged out")
}
if http.StatusFound != w.Code {
t.Errorf("Expected status code %d, got %d", http.StatusFound, w.Code)
}
location := w.Header().Get("Location")
if location != "/dashboard" {
t.Errorf("Expected lcoation %s, got %s", "/dashboard", location)
}
}
func TestAuth_logoutHandlerFunc_OtherMethods(t *testing.T) {
a, _ := testSetup()
methods := []string{"HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT"}
for i, method := range methods {
r, err := http.NewRequest(method, "/logout", nil)
if err != nil {
t.Errorf("%d> Unexpected error '%s'", i, err)
}
w := httptest.NewRecorder()
if err := a.logoutHandlerFunc(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
}
}
}

View File

@ -182,10 +182,18 @@ type MockClientStorer struct {
GetShouldFail bool
}
func NewMockClientStorer() *MockClientStorer {
return &MockClientStorer{
Values: make(map[string]string),
func NewMockClientStorer(data ...string) *MockClientStorer {
if len(data)%2 != 0 {
panic("It should be a key value list of arguments.")
}
values := make(map[string]string)
for i := 0; i < len(data)-1; i += 2 {
values[data[i]] = data[i+1]
}
return &MockClientStorer{Values: values}
}
func (m *MockClientStorer) Get(key string) (string, bool) {
@ -252,3 +260,19 @@ func (m *MockMailer) Send(email authboss.Email) error {
m.Last = email
return nil
}
type MockAfterCallback struct {
HasBeenCalled bool
Fn authboss.After
}
func NewMockAfterCallback() *MockAfterCallback {
m := MockAfterCallback{}
m.Fn = func(_ *authboss.Context) error {
m.HasBeenCalled = true
return nil
}
return &m
}

View File

@ -101,7 +101,7 @@ func confirm_email_txt_tpl() (*asset, error) {
return a, nil
}
var _login_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x7c\x92\x4d\x6b\xf3\x30\x0c\x80\xef\x85\xfe\x07\xe3\xfb\xdb\xfc\x81\x24\xf0\xc2\x2e\x83\x7d\x94\xad\xf7\xe1\x38\xca\x62\x1a\x5b\x41\x96\xfb\x41\xc8\x7f\x9f\xbd\xa4\x6b\x02\x63\x39\x05\x49\x7e\xf4\x58\xf2\x30\xec\x9a\x4e\xf9\xf6\xc3\x07\xad\xc1\xfb\x71\xdc\x6e\xf2\x06\xc9\x0a\xa5\xd9\xa0\x2b\x64\xd6\xe1\xa7\x71\x52\x58\xe0\x16\xeb\x42\xee\x5f\xdf\x0f\xb2\xdc\x6e\x44\xfc\x86\xc1\x34\x62\x07\x44\x48\xe3\x18\x51\xf3\x5f\x5e\x91\xc8\xca\x61\x00\x57\x27\x5e\xaa\xcc\x8d\xeb\x03\x0b\xbe\xf6\x50\x48\x86\x0b\x4b\xa1\x63\x5f\x5f\xc8\xd4\xec\x9f\x46\xc7\x84\x9d\x14\x4e\xd9\x58\x10\x51\x3d\x19\xab\xe8\xfa\xf8\x30\x8e\x52\xf4\x9d\xd2\xd0\x62\x57\x03\xa5\x24\x1b\xee\x40\xac\x4b\x4e\xaa\x0b\xd3\xc9\xe0\x81\x12\x26\x46\xcb\xc9\x64\xa5\x30\x3b\xf4\xb1\xf9\x19\xa9\xfe\xd3\xe3\x5e\xb4\x32\xd8\xdf\xc2\xbf\xf1\x27\x7c\x6b\xea\x1a\xdc\xe2\x3e\x17\x4f\xcd\xcb\x64\xb5\x70\x4d\xd1\x03\x1e\xc1\xa5\x70\xb6\x9a\xaa\x6f\xf1\xfc\x06\x16\x6c\x05\x69\xa4\x4b\xb8\x6e\x41\x1f\x2b\xbc\xdc\xf0\x64\x7f\x98\x4c\x01\x64\x29\x6e\x07\xc5\x33\xac\xd7\x50\x05\x66\x74\x33\xc7\x87\xca\x1a\x96\xe5\x53\xda\x70\x9e\x4d\xb9\xd5\x9d\x96\x2a\x1a\x4f\xdf\x26\x4a\xb4\x04\x4d\x7c\x18\x34\x85\x64\x39\xe7\xc4\x7f\xad\x31\x38\xce\x33\x75\x5f\x7e\x9e\xa5\xc1\x96\x5f\x01\x00\x00\xff\xff\xe7\xed\x2e\xa4\x68\x02\x00\x00")
var _login_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x7c\x92\xcb\x6a\xeb\x30\x10\x86\xf7\x81\xbc\x83\xd0\xfe\xc4\x2f\x60\x1b\x0e\x74\x53\xe8\x25\xb4\xa1\xdb\x22\xcb\xe3\x5a\xc4\xd2\x98\xd1\x38\x17\x8c\xdf\xbd\x52\xed\x34\x51\x29\xcd\x2a\xfe\x67\xf4\xe9\xd3\x48\xe3\xb8\x69\x3a\xe5\xdb\x77\x3f\x68\x0d\xde\x4f\xd3\x7a\x95\x37\x48\x56\x28\xcd\x06\x5d\x21\xb3\x0e\x3f\x8c\x93\xc2\x02\xb7\x58\x17\x72\xfb\xfc\xba\x93\xe5\x7a\x25\xc2\x6f\x1c\x4d\x23\x36\x40\x84\x34\x4d\x01\xb5\xfc\xcb\x2b\x12\x59\x39\x8e\xe0\xea\xc8\x8b\x9d\xb9\x71\xfd\xc0\x82\xcf\x3d\x14\x92\xe1\xc4\x52\xe8\xb0\xaf\x2f\x64\xdc\xec\x9f\x46\xc7\x84\x9d\x14\x4e\xd9\xd0\x10\x50\x3d\x19\xab\xe8\x7c\x7f\x37\x4d\x52\xf4\x9d\xd2\xd0\x62\x57\x03\xc5\x22\x1b\xee\x40\xa4\x2d\x07\xd5\x0d\x3f\x56\xbe\xc5\x28\xd4\xca\xd9\x27\x11\x59\x4c\xfa\xa0\x70\x44\xaa\xff\xb4\xb9\x36\x25\x1e\xdb\x4b\xfc\x1b\x7f\xc6\xb7\xa6\xae\xc1\xdd\x9c\xea\xe4\xa9\x79\x0a\x1f\xa9\x71\x4c\x77\xb8\x07\x17\xe3\x2c\x99\xad\x6f\xf1\xf8\x02\x16\x6c\x05\x71\xb0\xb7\x70\xdd\x82\xde\x57\x78\xba\xe0\xc9\x7e\x33\x99\x06\x90\xa5\xb8\x2c\x14\x8f\x90\x5e\x46\x35\x30\xa3\x5b\x38\x7e\xa8\xac\x61\x59\x3e\xc4\x7b\xce\xb3\xb9\x96\x9c\xe9\x56\x45\xe3\xe1\xcb\x44\x89\x96\xa0\x09\xcf\x83\xe6\x48\x96\x4b\x4d\xfc\xd7\x1a\x07\xc7\x79\xa6\xae\x4f\x20\xcf\xe2\x60\xcb\xcf\x00\x00\x00\xff\xff\x7f\x7f\xf0\x07\x6e\x02\x00\x00")
func login_tpl_bytes() ([]byte, error) {
return bindata_read(
@ -116,7 +116,7 @@ func login_tpl() (*asset, error) {
return nil, err
}
info := bindata_file_info{name: "login.tpl", size: 616, mode: os.FileMode(438), modTime: time.Unix(1424735294, 0)}
info := bindata_file_info{name: "login.tpl", size: 622, mode: os.FileMode(438), modTime: time.Unix(1424815807, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}

View File

@ -138,5 +138,5 @@ func Redirect(ctx *authboss.Context, w http.ResponseWriter, r *http.Request, pat
if len(flashError) > 0 {
ctx.SessionStorer.Put(authboss.FlashErrorKey, flashError)
}
http.Redirect(w, r, path, http.StatusTemporaryRedirect)
http.Redirect(w, r, path, http.StatusFound)
}

View File

@ -1,7 +1,7 @@
{{.flash_success}}
<form action="/login" method="POST">
{{if .error}}{{.error}}<br />{{end}}
<input type="text" class="form-control" name="{{.primaryID}}" placeholder="{{title .primaryID}}" value="{{.username}}"><br />
<input type="text" class="form-control" name="{{.primaryID}}" placeholder="{{title .primaryID}}" value="{{.primaryIDValue}}"><br />
<input type="password" class="form-control" name="password" placeholder="Password"><br />
<input type="hidden" name="{{.xsrfName}}" value="{{.xsrfToken}}" />
{{if .showRemember}}<input type="checkbox" name="rm" value="true"> Remember Me{{end}}

1
recover/recover_test.go Normal file
View File

@ -0,0 +1 @@
package recover