From d4f8d2f292eb4a2bdc43b1b3eb6d210ca7ccdb65 Mon Sep 17 00:00:00 2001 From: Aaron L Date: Tue, 20 Feb 2018 08:58:59 -0800 Subject: [PATCH] Finish auth module --- auth/auth.go | 47 +-- auth/auth_test.go | 690 ++++++++++++++++++++-------------------- defaults/responder.go | 4 +- internal/mocks/mocks.go | 226 ++++++++++++- 4 files changed, 592 insertions(+), 375 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index 23b481b..15b04a4 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -41,7 +41,7 @@ func (a *Auth) Init(ab *authboss.Authboss) (err error) { case "DELETE": logoutRouteMethod = a.Authboss.Config.Core.Router.Delete default: - return errors.Errorf("auth wants to register a logout route but is given an invalid method: %s", a.Authboss.Config.Modules.AuthLogoutMethod) + return errors.Errorf("auth wants to register a logout route but was given an invalid method: %s", a.Authboss.Config.Modules.AuthLogoutMethod) } a.Authboss.Config.Core.Router.Get("/login", a.Authboss.Core.ErrorHandler.Wrap(a.LoginGet)) @@ -59,6 +59,8 @@ func (a *Auth) LoginGet(w http.ResponseWriter, r *http.Request) error { // LoginPost attempts to validate the credentials passed in // to log in a user. func (a *Auth) LoginPost(w http.ResponseWriter, r *http.Request) error { + logger := a.RequestLogger(r) + validatable, err := a.Authboss.Core.BodyReader.Read(PageLogin, r) if err != nil { return err @@ -71,6 +73,7 @@ func (a *Auth) LoginPost(w http.ResponseWriter, r *http.Request) error { pid := creds.GetPID() pidUser, err := a.Authboss.Storage.Server.Load(r.Context(), pid) if err == authboss.ErrUserNotFound { + logger.Infof("failed to load user requested by pid: %s", pid) data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"} return a.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data) } else if err != nil { @@ -78,45 +81,43 @@ func (a *Auth) LoginPost(w http.ResponseWriter, r *http.Request) error { } authUser := authboss.MustBeAuthable(pidUser) - password, err := authUser.GetPassword(r.Context()) - if err != nil { - return err - } + password := authUser.GetPassword() + var handled bool err = bcrypt.CompareHashAndPassword([]byte(password), []byte(creds.GetPassword())) if err != nil { - err = a.Authboss.Events.FireAfter(r.Context(), authboss.EventAuthFail) + handled, err = a.Authboss.Events.FireAfter(authboss.EventAuthFail, w, r) if err != nil { return err + } else if handled { + return nil } + logger.Infof("user %s failed to log in", pid) data := authboss.HTMLData{authboss.DataErr: "Invalid Credentials"} return a.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data) } - interrupted, err := a.Events.FireBefore(r.Context(), authboss.EventAuth) + handled, err = a.Events.FireBefore(authboss.EventAuth, w, r) if err != nil { return err - } else if interrupted != authboss.InterruptNone { - var reason string - switch interrupted { - case authboss.InterruptAccountLocked: - reason = "Your account is locked" - case authboss.InterruptAccountNotConfirmed: - reason = "Your account is not confirmed" - } - data := authboss.HTMLData{authboss.DataErr: reason} - return a.Authboss.Core.Responder.Respond(w, r, http.StatusOK, PageLogin, data) + } else if handled { + return nil } + logger.Infof("user %s logged in", pid) authboss.PutSession(w, authboss.SessionKey, pid) authboss.DelSession(w, authboss.SessionHalfAuthKey) - if err := a.Authboss.Events.FireAfter(r.Context(), authboss.EventAuth); err != nil { + handled, err = a.Authboss.Events.FireAfter(authboss.EventAuth, w, r) + if err != nil { return err + } else if handled { + return nil } ro := authboss.RedirectOptions{ + Code: http.StatusTemporaryRedirect, RedirectPath: a.Authboss.Paths.AuthLogoutOK, } return a.Authboss.Core.Redirector.Redirect(w, r, ro) @@ -124,11 +125,21 @@ func (a *Auth) LoginPost(w http.ResponseWriter, r *http.Request) error { // Logout a user func (a *Auth) Logout(w http.ResponseWriter, r *http.Request) error { + logger := a.RequestLogger(r) + user, err := a.CurrentUser(w, r) + if err != nil { + return err + } + + logger.Infof("user %s logged out", user.GetPID()) + authboss.DelSession(w, authboss.SessionKey) authboss.DelSession(w, authboss.SessionLastAction) + authboss.DelSession(w, authboss.SessionHalfAuthKey) authboss.DelCookie(w, authboss.CookieRemember) ro := authboss.RedirectOptions{ + Code: http.StatusTemporaryRedirect, RedirectPath: a.Authboss.Paths.AuthLogoutOK, Success: "You have been logged out", } diff --git a/auth/auth_test.go b/auth/auth_test.go index 39e94c6..609c7e7 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -1,380 +1,388 @@ package auth import ( - "bytes" - "html/template" - "io/ioutil" "net/http" "net/http/httptest" - "strings" "testing" - "github.com/pkg/errors" - "github.com/volatiletech/authboss" "github.com/volatiletech/authboss/internal/mocks" ) -func testSetup() (a *Auth, s *mocks.MockStorer) { - s = mocks.NewMockStorer() - - ab := authboss.New() - ab.LogWriter = ioutil.Discard - ab.Layout = template.Must(template.New("").Parse(`{{template "authboss" .}}`)) - ab.Storage.Server = s - ab.XSRFName = "xsrf" - ab.XSRFMaker = func(_ http.ResponseWriter, _ *http.Request) string { - return "xsrfvalue" - } - ab.PrimaryID = authboss.StoreUsername - - a = &Auth{} - if err := a.Initialize(ab); err != nil { - panic(err) - } - - return a, s -} - -func testRequest(ab *authboss.Authboss, method string, postFormValues ...string) (*authboss.Context, *httptest.ResponseRecorder, *http.Request, authboss.ClientStorerErr) { - sessionStorer := mocks.NewMockClientStorer() - ctx := ab.NewContext() - r := mocks.MockRequest(method, postFormValues...) - ctx.SessionStorer = sessionStorer - - return ctx, httptest.NewRecorder(), r, sessionStorer -} - -func TestAuth(t *testing.T) { - t.Parallel() - - a, _ := testSetup() - - storage := a.Storage() - if storage[a.PrimaryID] != authboss.String { - t.Error("Expected storage KV:", a.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(t *testing.T) { - t.Parallel() - - a, _ := testSetup() - ctx, w, r, _ := testRequest(a.Authboss, "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, " 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) { +func TestAuthInit(t *testing.T) { t.Parallel() ab := authboss.New() - storer := mocks.NewMockStorer() - ab.Storage.Server = storer - ctx := ab.NewContext() - storer.Users["john"] = authboss.Attributes{"password": "$2a$10$pgFsuQwdhwOdZp/v52dvHeEi53ZaI7dGmtwK4bAzGGN5A4nT6doqm"} - if _, err := validateCredentials(ctx, "john", "a"); err != nil { - t.Error("Unexpected error:", err) + router := &mocks.Router{} + renderer := &mocks.Renderer{} + errHandler := &mocks.ErrorHandler{} + ab.Config.Core.Router = router + ab.Config.Core.ViewRenderer = renderer + ab.Config.Core.ErrorHandler = errHandler + + a := &Auth{} + a.Init(ab) + + if err := renderer.HasLoadedViews(PageLogin); err != nil { + t.Error(err) } - ctx = ab.NewContext() - if valid, err := validateCredentials(ctx, "jake", "a"); err != nil { - t.Error("Expect no error when user not found:", err) - } else if valid { - t.Error("Expect invalid when not user found") + if err := router.HasGets("/login"); err != nil { + t.Error(err) } - - ctx = ab.NewContext() - storer.GetErr = "Failed to load user" - if _, err := validateCredentials(ctx, "", ""); err.Error() != "Failed to load user" { - t.Error("Unexpected error:", err) + if err := router.HasPosts("/login"); err != nil { + t.Error(err) + } + if err := router.HasDeletes("/logout"); err != nil { + t.Error(err) } - } -func TestAuth_logoutHandlerFunc_GET(t *testing.T) { +func TestAuthGet(t *testing.T) { t.Parallel() - a, _ := testSetup() + ab := authboss.New() + responder := &mocks.Responder{} + ab.Config.Core.Responder = responder - a.AuthLogoutOKPath = "/dashboard" + a := &Auth{ab} + a.LoginGet(nil, nil) - ctx, w, r, sessionStorer := testRequest(a.Authboss, "GET") - sessionStorer.Put(authboss.SessionKey, "asdf") - sessionStorer.Put(authboss.SessionLastAction, "1234") - - cookieStorer := mocks.NewMockClientStorer(authboss.CookieRemember, "qwert") - ctx.CookieStorer = cookieStorer - - if err := a.logoutHandlerFunc(ctx, w, r); err != nil { - t.Error("Unexpected error:", err) + if responder.Page != PageLogin { + t.Error("wanted login page, got:", responder.Page) } - if val, ok := sessionStorer.Get(authboss.SessionKey); ok { - t.Error("Unexpected session key:", val) - } - - if val, ok := sessionStorer.Get(authboss.SessionLastAction); ok { - t.Error("Unexpected last action:", val) - } - - if val, ok := cookieStorer.Get(authboss.CookieRemember); ok { - t.Error("Unexpected rm cookie:", val) - } - - 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) + if responder.Status != http.StatusOK { + t.Error("wanted ok status, got:", responder.Status) } } -func TestAuth_logoutHandlerFunc_OtherMethods(t *testing.T) { - a, _ := testSetup() +type testHarness struct { + auth *Auth + ab *authboss.Authboss - methods := []string{"HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT"} + bodyReader *mocks.BodyReader + responder *mocks.Responder + redirector *mocks.Redirector + session *mocks.ClientStateRW + storer *mocks.ServerStorer +} - for i, method := range methods { - r, err := http.NewRequest(method, "/logout", nil) - if err != nil { - t.Errorf("%d> Unexpected error '%s'", i, err) +func testSetup() *testHarness { + harness := &testHarness{} + + harness.ab = authboss.New() + harness.bodyReader = &mocks.BodyReader{} + harness.redirector = &mocks.Redirector{} + harness.responder = &mocks.Responder{} + harness.session = mocks.NewClientRW() + harness.storer = mocks.NewServerStorer() + + harness.ab.Config.Core.BodyReader = harness.bodyReader + harness.ab.Config.Core.Logger = mocks.Logger{} + harness.ab.Config.Core.Responder = harness.responder + harness.ab.Config.Core.Redirector = harness.redirector + harness.ab.Config.Storage.SessionState = harness.session + harness.ab.Config.Storage.Server = harness.storer + + harness.auth = &Auth{harness.ab} + + return harness +} + +func TestAuthPostSuccess(t *testing.T) { + t.Parallel() + + setupMore := func(h *testHarness) *testHarness { + h.bodyReader.Return = mocks.Values{ + PID: "test@test.com", + Password: "hello world", } - w := httptest.NewRecorder() + h.storer.Users["test@test.com"] = &mocks.User{ + Email: "test@test.com", + Password: "$2a$10$IlfnqVyDZ6c1L.kaA/q3bu1nkAC6KukNUsizvlzay1pZPXnX2C9Ji", // hello world + } + h.session.ClientValues[authboss.SessionHalfAuthKey] = "true" - if err := a.logoutHandlerFunc(nil, w, r); err != nil { - t.Errorf("%d> Unexpected error: %s", i, err) + return h + } + + t.Run("normal", func(t *testing.T) { + t.Parallel() + h := setupMore(testSetup()) + + var beforeCalled, afterCalled bool + h.ab.Events.Before(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) { + beforeCalled = true + return false, nil + }) + h.ab.Events.After(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) { + afterCalled = true + return false, nil + }) + + r := mocks.Request("POST") + resp := httptest.NewRecorder() + w := h.ab.NewResponse(resp, r) + + if err := h.auth.LoginPost(w, r); err != nil { + t.Error(err) } - if http.StatusMethodNotAllowed != w.Code { - t.Errorf("%d> Expected status code %d, got %d", i, http.StatusMethodNotAllowed, w.Code) - continue + if resp.Code != http.StatusTemporaryRedirect { + t.Error("code was wrong:", resp.Code) } + + if _, ok := h.session.ClientValues[authboss.SessionHalfAuthKey]; ok { + t.Error("half auth should have been deleted") + } + if pid := h.session.ClientValues[authboss.SessionKey]; pid != "test@test.com" { + t.Error("pid was wrong:", pid) + } + + if !beforeCalled { + t.Error("before should have been called") + } + if !afterCalled { + t.Error("after should have been called") + } + }) + + t.Run("handledBefore", func(t *testing.T) { + t.Parallel() + h := setupMore(testSetup()) + + var beforeCalled bool + h.ab.Events.Before(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) { + w.WriteHeader(http.StatusTeapot) + beforeCalled = true + return true, nil + }) + + r := mocks.Request("POST") + resp := httptest.NewRecorder() + w := h.ab.NewResponse(resp, r) + + if err := h.auth.LoginPost(w, r); err != nil { + t.Error(err) + } + + if h.responder.Status != 0 { + t.Error("a status should never have been sent back") + } + if _, ok := h.session.ClientValues[authboss.SessionKey]; ok { + t.Error("session key should not have been set") + } + + if !beforeCalled { + t.Error("before should have been called") + } + if resp.Code != http.StatusTeapot { + t.Error("should have left the response alone once teapot was sent") + } + }) + + t.Run("handledAfter", func(t *testing.T) { + t.Parallel() + h := setupMore(testSetup()) + + var afterCalled bool + h.ab.Events.After(authboss.EventAuth, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) { + w.WriteHeader(http.StatusTeapot) + afterCalled = true + return true, nil + }) + + r := mocks.Request("POST") + resp := httptest.NewRecorder() + w := h.ab.NewResponse(resp, r) + + if err := h.auth.LoginPost(w, r); err != nil { + t.Error(err) + } + + if h.responder.Status != 0 { + t.Error("a status should never have been sent back") + } + if _, ok := h.session.ClientValues[authboss.SessionKey]; !ok { + t.Error("session key should have been set") + } + + if !afterCalled { + t.Error("after should have been called") + } + if resp.Code != http.StatusTeapot { + t.Error("should have left the response alone once teapot was sent") + } + }) +} + +func TestAuthPostBadPassword(t *testing.T) { + t.Parallel() + + setupMore := func(h *testHarness) *testHarness { + h.bodyReader.Return = mocks.Values{ + PID: "test@test.com", + Password: "world hello", + } + h.storer.Users["test@test.com"] = &mocks.User{ + Email: "test@test.com", + Password: "$2a$10$IlfnqVyDZ6c1L.kaA/q3bu1nkAC6KukNUsizvlzay1pZPXnX2C9Ji", // hello world + } + + return h + } + + t.Run("normal", func(t *testing.T) { + h := setupMore(testSetup()) + + r := mocks.Request("POST") + resp := httptest.NewRecorder() + w := h.ab.NewResponse(resp, r) + + var afterCalled bool + h.ab.Events.After(authboss.EventAuthFail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) { + afterCalled = true + return false, nil + }) + + if err := h.auth.LoginPost(w, r); err != nil { + t.Error(err) + } + + if resp.Code != 200 { + t.Error("wanted a 200:", resp.Code) + } + + if h.responder.Data[authboss.DataErr] != "Invalid Credentials" { + t.Error("wrong error:", h.responder.Data) + } + + if _, ok := h.session.ClientValues[authboss.SessionKey]; ok { + t.Error("user should not be logged in") + } + + if !afterCalled { + t.Error("after should have been called") + } + }) + + t.Run("handledAfter", func(t *testing.T) { + h := setupMore(testSetup()) + + r := mocks.Request("POST") + resp := httptest.NewRecorder() + w := h.ab.NewResponse(resp, r) + + var afterCalled bool + h.ab.Events.After(authboss.EventAuthFail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) { + w.WriteHeader(http.StatusTeapot) + afterCalled = true + return true, nil + }) + + if err := h.auth.LoginPost(w, r); err != nil { + t.Error(err) + } + + if h.responder.Status != 0 { + t.Error("responder should not have been called to give a status") + } + if _, ok := h.session.ClientValues[authboss.SessionKey]; ok { + t.Error("user should not be logged in") + } + + if !afterCalled { + t.Error("after should have been called") + } + if resp.Code != http.StatusTeapot { + t.Error("should have left the response alone once teapot was sent") + } + }) +} + +func TestAuthPostUserNotFound(t *testing.T) { + t.Parallel() + + harness := testSetup() + harness.bodyReader.Return = mocks.Values{ + PID: "test@test.com", + Password: "world hello", + } + + r := mocks.Request("POST") + resp := httptest.NewRecorder() + w := harness.ab.NewResponse(resp, r) + + // This event is really the only thing that separates "user not found" from "bad password" + var afterCalled bool + harness.ab.Events.After(authboss.EventAuthFail, func(w http.ResponseWriter, r *http.Request, handled bool) (bool, error) { + afterCalled = true + return false, nil + }) + + if err := harness.auth.LoginPost(w, r); err != nil { + t.Error(err) + } + + if resp.Code != 200 { + t.Error("wanted a 200:", resp.Code) + } + + if harness.responder.Data[authboss.DataErr] != "Invalid Credentials" { + t.Error("wrong error:", harness.responder.Data) + } + + if _, ok := harness.session.ClientValues[authboss.SessionKey]; ok { + t.Error("user should not be logged in") + } + + if afterCalled { + t.Error("after should not have been called") + } +} + +func TestAuthLogout(t *testing.T) { + t.Parallel() + + h := testSetup() + h.storer.Users["test@test.com"] = &mocks.User{ + Email: "test@test.com", + Password: "$2a$10$IlfnqVyDZ6c1L.kaA/q3bu1nkAC6KukNUsizvlzay1pZPXnX2C9Ji", // hello world + } + + h.session.ClientValues[authboss.SessionKey] = "test@test.com" + h.session.ClientValues[authboss.SessionHalfAuthKey] = "true" + + cookies := mocks.NewClientRW() + cookies.ClientValues[authboss.CookieRemember] = "token" + h.ab.Config.Storage.CookieState = cookies + + r := mocks.Request("POST") + resp := httptest.NewRecorder() + w := h.ab.NewResponse(resp, r) + + var err error + r, err = h.ab.LoadClientState(w, r) + if err != nil { + t.Error(err) + } + + if err := h.auth.Logout(w, r); err != nil { + t.Error(err) + } + + if _, ok := h.session.ClientValues[authboss.SessionKey]; ok { + t.Error("want session key gone") + } + if _, ok := h.session.ClientValues[authboss.SessionHalfAuthKey]; ok { + t.Error("want session half auth key gone") + } + if _, ok := h.session.ClientValues[authboss.SessionLastAction]; ok { + t.Error("want session last action") + } + if _, ok := cookies.ClientValues[authboss.CookieRemember]; ok { + t.Error("want remember me cookies gone") } } diff --git a/defaults/responder.go b/defaults/responder.go index 22d48f4..2ce04c2 100644 --- a/defaults/responder.go +++ b/defaults/responder.go @@ -24,13 +24,13 @@ func NewResponder(renderer authboss.Renderer) *Responder { // Respond to an HTTP request. It's main job is to merge data that comes in from // various middlewares via the context with the data sent by the controller and render that. -func (r *Responder) Respond(w http.ResponseWriter, req *http.Request, code int, templateName string, data authboss.HTMLData) error { +func (r *Responder) Respond(w http.ResponseWriter, req *http.Request, code int, page string, data authboss.HTMLData) error { ctxData := req.Context().Value(authboss.CTXKeyData) if ctxData != nil { data.Merge(ctxData.(authboss.HTMLData)) } - rendered, mime, err := r.Renderer.Render(req.Context(), templateName, data) + rendered, mime, err := r.Renderer.Render(req.Context(), page, data) if err != nil { return err } diff --git a/internal/mocks/mocks.go b/internal/mocks/mocks.go index 4d65acc..3dc3ecf 100644 --- a/internal/mocks/mocks.go +++ b/internal/mocks/mocks.go @@ -30,8 +30,8 @@ type User struct { OAuthExpiry time.Time } -func (m User) GetUsername() string { return m.Username } func (m User) GetPID() string { return m.Email } +func (m User) GetUsername() string { return m.Username } func (m User) GetPassword() string { return m.Password } func (m User) GetRecoverToken() string { return m.RecoverToken } func (m User) GetRecoverTokenExpiry() time.Time { return m.RecoverTokenExpiry } @@ -44,21 +44,22 @@ func (m User) GetOAuthToken() string { return m.OAuthToken } func (m User) GetOAuthRefresh() string { return m.OAuthRefresh } func (m User) GetOAuthExpiry() time.Time { return m.OAuthExpiry } -func (m *User) SetUsername(username string) { m.Username = username } -func (m *User) SetEmail(email string) { m.Email = email } -func (m *User) SetPassword(password string) { m.Password = password } -func (m *User) SetRecoverToken(recoverToken string) { m.RecoverToken = recoverToken } -func (m *User) SetRecoverTokenExpiry(recoverTokenExpiry time.Time) { +func (m *User) PutPID(email string) { m.Email = email } +func (m *User) PutUsername(username string) { m.Username = username } +func (m *User) PutEmail(email string) { m.Email = email } +func (m *User) PutPassword(password string) { m.Password = password } +func (m *User) PutRecoverToken(recoverToken string) { m.RecoverToken = recoverToken } +func (m *User) PutRecoverTokenExpiry(recoverTokenExpiry time.Time) { m.RecoverTokenExpiry = recoverTokenExpiry } -func (m *User) SetConfirmToken(confirmToken string) { m.ConfirmToken = confirmToken } -func (m *User) SetConfirmed(confirmed bool) { m.Confirmed = confirmed } -func (m *User) SetLocked(locked bool) { m.Locked = locked } -func (m *User) SetAttemptNumber(attemptNumber int) { m.AttemptNumber = attemptNumber } -func (m *User) SetAttemptTime(attemptTime time.Time) { m.AttemptTime = attemptTime } -func (m *User) SetOAuthToken(oAuthToken string) { m.OAuthToken = oAuthToken } -func (m *User) SetOAuthRefresh(oAuthRefresh string) { m.OAuthRefresh = oAuthRefresh } -func (m *User) SetOAuthExpiry(oAuthExpiry time.Time) { m.OAuthExpiry = oAuthExpiry } +func (m *User) PutConfirmToken(confirmToken string) { m.ConfirmToken = confirmToken } +func (m *User) PutConfirmed(confirmed bool) { m.Confirmed = confirmed } +func (m *User) PutLocked(locked bool) { m.Locked = locked } +func (m *User) PutAttemptNumber(attemptNumber int) { m.AttemptNumber = attemptNumber } +func (m *User) PutAttemptTime(attemptTime time.Time) { m.AttemptTime = attemptTime } +func (m *User) PutOAuthToken(oAuthToken string) { m.OAuthToken = oAuthToken } +func (m *User) PutOAuthRefresh(oAuthRefresh string) { m.OAuthRefresh = oAuthRefresh } +func (m *User) PutOAuthExpiry(oAuthExpiry time.Time) { m.OAuthExpiry = oAuthExpiry } // ServerStorer should be valid for any module storer defined in authboss. type ServerStorer struct { @@ -74,6 +75,23 @@ func NewServerStorer() *ServerStorer { } } +// Load a user +func (s *ServerStorer) Load(ctx context.Context, key string) (authboss.User, error) { + user, ok := s.Users[key] + if ok { + return user, nil + } + + return nil, authboss.ErrUserNotFound +} + +// Save a user +func (s *ServerStorer) Save(ctx context.Context, user authboss.User) error { + u := user.(*User) + s.Users[u.Email] = u + return nil +} + /* // TODO(aarondl): What is this? // AddToken for remember me @@ -315,3 +333,183 @@ func NewAfterCallback() *AfterCallback { return &m } + +// Renderer mock +type Renderer struct { + Pages []string + + // Render call variables + Context context.Context + Page string + Data authboss.HTMLData +} + +// HasLoadedViews ensures the views were loaded +func (r *Renderer) HasLoadedViews(pages ...string) error { + if len(r.Pages) != len(pages) { + return errors.Errorf("want: %d loaded views, got: %d", len(pages), len(r.Pages)) + } + + for i, want := range pages { + got := r.Pages[i] + if want != got { + return errors.Errorf("want: %s [%d], got: %s", want, i, got) + } + } + + return nil +} + +// Load nothing but store the pages that were loaded +func (r *Renderer) Load(pages ...string) error { + r.Pages = append(r.Pages, pages...) + return nil +} + +// Render nothing, but record the arguments into the renderer +func (r *Renderer) Render(ctx context.Context, page string, data authboss.HTMLData) ([]byte, string, error) { + r.Context = ctx + r.Page = page + r.Data = data + return nil, "text/html", nil +} + +// Responder records how a request was responded to +type Responder struct { + Status int + Page string + Data authboss.HTMLData +} + +// Respond stores the arguments in the struct +func (r *Responder) Respond(w http.ResponseWriter, req *http.Request, code int, page string, data authboss.HTMLData) error { + r.Status = code + r.Page = page + r.Data = data + + return nil +} + +// Redirector stores the redirect options passed to it and writes the Code +// to the ResponseWriter. +type Redirector struct { + Options authboss.RedirectOptions +} + +// Redirect a request +func (r *Redirector) Redirect(w http.ResponseWriter, req *http.Request, ro authboss.RedirectOptions) error { + r.Options = ro + http.Redirect(w, req, ro.RedirectPath, ro.Code) + return nil +} + +// BodyReader reads the body of a request and returns some values +type BodyReader struct { + Return Values +} + +// Read the return values +func (b BodyReader) Read(page string, r *http.Request) (authboss.Validator, error) { + return b.Return, nil +} + +// Values is returned from the BodyReader +type Values struct { + PID string + Password string +} + +// GetPID from values +func (v Values) GetPID() string { + return v.PID +} + +// GetPassword from values +func (v Values) GetPassword() string { + return v.Password +} + +// Validate the values +func (v Values) Validate() []error { + return nil +} + +// Logger logs to the void +type Logger struct { +} + +// Info logging +func (l Logger) Info(string) {} + +// Error logging +func (l Logger) Error(string) {} + +// Router records the routes that were registered +type Router struct { + Gets []string + Posts []string + Deletes []string +} + +// ServeHTTP does nothing +func (Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { +} + +// Get records the path in the router +func (r *Router) Get(path string, _ http.Handler) { + r.Gets = append(r.Gets, path) +} + +// Post records the path in the router +func (r *Router) Post(path string, _ http.Handler) { + r.Posts = append(r.Posts, path) +} + +// Delete records the path in the router +func (r *Router) Delete(path string, _ http.Handler) { + r.Deletes = append(r.Deletes, path) +} + +// HasGets ensures all gets routes are present +func (r *Router) HasGets(gets ...string) error { + return r.hasRoutes(gets, r.Gets) +} + +// HasPosts ensures all gets routes are present +func (r *Router) HasPosts(posts ...string) error { + return r.hasRoutes(posts, r.Posts) +} + +// HasDeletes ensures all gets routes are present +func (r *Router) HasDeletes(deletes ...string) error { + return r.hasRoutes(deletes, r.Deletes) +} + +func (r *Router) hasRoutes(want []string, got []string) error { + if len(got) != len(want) { + return errors.Errorf("want: %d get routes, got: %d", len(want), len(got)) + } + + for i, w := range want { + g := got[i] + if w != g { + return errors.Errorf("wanted route: %s [%d], but got: %s", w, i, g) + } + } + + return nil +} + +// ErrorHandler just holds the last error +type ErrorHandler struct { + Error error +} + +// Wrap an http method +func (e *ErrorHandler) Wrap(handler func(w http.ResponseWriter, r *http.Request) error) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := handler(w, r); err != nil { + e.Error = err + } + }) +}