From 57c9282cbd7825e57db25715d7811d99d4111e9d Mon Sep 17 00:00:00 2001 From: Kris Runzer Date: Sun, 8 Feb 2015 23:08:33 -0800 Subject: [PATCH] First part of recover module reworking --- config.go | 19 + internal/mocks/mocks.go | 32 +- internal/views/bindata.go | 62 ++- internal/views/templates/layout.tpl | 44 ++ internal/views/templates/layoutEmail.tpl | 1 + internal/views/templates/login.tpl | 40 +- internal/views/templates/recover-complete.tpl | 25 ++ internal/views/templates/recover.tpl | 27 +- internal/views/views.go | 21 +- internal/views/views_test.go | 12 +- recover/recover.go | 146 +++--- recover/recover_test.go | 424 ++++++++++++++++-- 12 files changed, 711 insertions(+), 142 deletions(-) create mode 100644 internal/views/templates/layout.tpl create mode 100644 internal/views/templates/layoutEmail.tpl diff --git a/config.go b/config.go index 535e16f..8f871d5 100644 --- a/config.go +++ b/config.go @@ -8,6 +8,12 @@ import ( "time" "golang.org/x/crypto/bcrypt" + "gopkg.in/authboss.v0/internal/views" +) + +const ( + layoutTpl = "layout.tpl" + layoutEmailTpl = "layoutEmail.tpl" ) // Config holds all the configuration for both authboss and it's modules. @@ -60,12 +66,25 @@ type Config struct { // NewConfig creates a new config full of default values ready to override. func NewConfig() *Config { + layout, err := views.AssetToTemplate(layoutTpl) + if err != nil { + panic(err) + } + + layoutEmail, err := views.AssetToTemplate(layoutEmailTpl) + if err != nil { + panic(err) + } + return &Config{ MountPath: "/", ViewsPath: "/", HostName: "localhost:8080", BCryptCost: bcrypt.DefaultCost, + Layout: layout, + LayoutEmail: layoutEmail, + AuthLogoutRoute: "/", AuthLoginSuccessRoute: "/", diff --git a/internal/mocks/mocks.go b/internal/mocks/mocks.go index 1972207..b8fee91 100644 --- a/internal/mocks/mocks.go +++ b/internal/mocks/mocks.go @@ -9,6 +9,7 @@ import ( ) type MockUser struct { + Username string Email string Password string } @@ -42,9 +43,25 @@ func (m *MockStorer) Put(key string, attr authboss.Attributes) error { } func (m *MockStorer) Get(key string, attrMeta authboss.AttributeMeta) (result interface{}, err error) { - return &MockUser{ - m.Users[key]["username"].(string), m.Users[key]["password"].(string), - }, nil + if _, ok := m.Users[key]; !ok { + return nil, authboss.ErrUserNotFound + } + + u := &MockUser{} + + if val, ok := m.Users[key]["username"]; ok { + u.Username = val.(string) + } + + if val, ok := m.Users[key]["email"]; ok { + u.Email = val.(string) + } + + if val, ok := m.Users[key]["password"]; ok { + u.Password = val.(string) + } + + return u, nil } func (m *MockStorer) AddToken(key, token string) error { @@ -105,3 +122,12 @@ func MockRequestContext(postKeyValues ...string) *authboss.Context { return ctx } + +type MockMailer struct { + Last authboss.Email +} + +func (m *MockMailer) Send(email authboss.Email) error { + m.Last = email + return nil +} diff --git a/internal/views/bindata.go b/internal/views/bindata.go index 704b58a..6728f4f 100644 --- a/internal/views/bindata.go +++ b/internal/views/bindata.go @@ -61,7 +61,47 @@ func (fi bindata_file_info) Sys() interface{} { return nil } -var _login_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x8c\x51\x4d\x4f\xc4\x20\x10\xbd\x9b\xf8\x1f\xc8\x78\xde\x34\xde\x69\x8f\x9e\xf6\x60\x4c\xfc\x01\xb4\xcc\x16\x12\x5a\x70\x80\xd5\xfd\xf7\x4e\x2b\xa0\x8d\x31\x91\xcb\xbc\xf9\xe0\xf1\x78\x23\x4d\x5a\xdc\x70\x7f\x27\x47\xaf\x6f\x5b\xd4\xf6\x2a\x26\xa7\x62\xec\xc1\xf9\xd9\xae\xa7\x49\x91\x06\xee\x08\x3e\xd2\x3c\x0e\x67\x3f\x9f\xec\x2a\x3b\x86\x72\xa4\xda\xb8\x78\x5a\x0a\xde\x73\xbb\x86\x9c\x44\xba\x05\xec\x21\xe1\x47\x02\xb1\xaa\x85\x71\x8e\x48\x1b\x02\x11\x9c\x9a\xd0\x78\xa7\x91\x7a\x78\x6d\x65\xc2\xb7\x6c\x09\x35\x5f\xa3\x8c\xf0\x17\x67\x60\x85\xef\x9e\x95\x15\xde\xef\xfc\xc0\xfb\xdc\xca\xff\xe4\x8d\x79\x5c\x6c\x53\xbb\x3b\x00\x07\x3f\xc4\x97\x2b\x75\xee\xaa\x5c\xe6\xc1\xf3\x3e\x58\xcd\xe8\x8a\x1b\x25\xfd\x65\xa9\x41\x17\x0e\x0a\x94\x30\x84\x97\x1e\x1e\x60\x78\xc1\xd9\xc6\x84\x24\x3b\x35\xfc\xac\x3f\x79\x9a\x7d\x12\xf5\x43\x5b\xbb\xbe\xc6\xfc\xdb\xe6\x5a\xac\xab\xec\xca\x6e\x3f\x03\x00\x00\xff\xff\x05\x40\xcd\x07\xe4\x01\x00\x00") +var _layout_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xd4\x95\xc1\x72\xd3\x30\x10\x86\xef\x79\x0a\x8d\x0e\xdc\x2c\xd1\x26\xc0\x4c\xeb\xe4\x06\x2f\xc0\x13\xc8\xd6\x3a\x56\x90\x25\xa3\x5d\xb7\xc9\x78\xfa\xee\xc8\x96\x1b\x5c\xd7\x40\x87\x03\x33\xe4\x60\x67\x37\xda\xff\xdf\xfd\x34\x52\xf2\x9a\x1a\x7b\xd8\xe4\x35\x28\x7d\xd8\xb0\xf8\xc9\xad\x71\xdf\x58\x1d\xa0\xda\x73\x29\x1d\x90\x76\x4a\x14\xde\x13\x52\x50\x6d\xa9\x9d\x28\x7d\x23\x2b\xef\x28\x53\x8f\x80\xbe\x01\xb9\x13\xef\xc5\x56\x96\x88\x2f\xd2\x22\x26\x38\x0b\x60\xf7\x1c\xe9\x62\x01\x6b\x00\xe2\x4c\xae\xd9\x34\xea\x3c\x28\xbf\xb2\xb9\x26\xe4\x56\x6c\xc5\xcd\xe8\x71\xcd\x89\xc6\xb8\x3f\x98\x60\x19\x4c\x4b\x0c\x43\xb9\xe7\x35\x51\x8b\x77\x52\xaa\x93\x3a\x8b\xa3\xf7\x47\x0b\xaa\x35\x38\xfa\x0c\x39\x69\x4d\x81\xf2\xf4\xbd\x83\x70\x91\xb7\xe2\x26\x8e\x94\x82\xd1\xe7\x84\xfc\x90\xcb\xa4\xb7\x22\xfe\xd6\x11\x6e\xe5\x69\x39\x41\x54\x66\x74\x69\x61\xcf\x09\xce\x24\x4f\xea\x41\x25\xe5\xb9\x61\x2e\xd3\x0e\xe5\x85\xd7\x97\xf8\xd2\xe6\x81\x95\x56\x21\xee\x79\x19\x99\x2b\xe3\x20\x64\x95\xed\x8c\xe6\xa9\xbb\xbe\x37\x15\x13\x5f\xe2\x92\xfa\x6b\x57\x96\x80\xf8\xf4\x94\xda\x9e\x95\x06\xff\x38\x2d\x5f\xfe\x52\x7a\x9b\x9d\x31\xf3\x55\x85\x40\xd9\x96\x0d\x71\xa3\xb3\x8f\xb3\xe5\xcb\x12\x65\x21\x10\x1b\x9f\x19\x26\xcb\x29\xd2\x06\x1b\x83\xa8\x0a\x0b\x9c\x8d\xdb\xb4\xe7\x8d\x0a\x47\xe3\x32\xf2\xed\x1d\xfb\xf4\xa1\x3d\xdf\x2f\x94\x47\xf5\xa2\x23\xf2\x6e\xa2\x93\x02\x7e\xed\xd0\x7a\x8c\x72\x5a\x91\x7a\x36\x98\x7a\x88\xdc\xb0\x55\xee\xf0\x8e\x4c\x03\x78\x1f\x21\x0e\x51\x2e\x93\xc0\x6b\x9b\xbe\x6f\x83\x71\xb4\x4a\xeb\xda\x8a\x8c\x93\xce\x50\xfd\x0c\x67\x5f\xfb\x1e\x9c\x9e\x0a\x67\xfc\x3f\x87\xe0\xc3\xbf\xa4\xaf\x95\x3b\x42\xf8\x1f\xe1\xcf\x51\xfd\x35\xfa\x25\xe3\xb5\xa9\x6f\x17\x53\x2f\xe9\x47\xda\x13\xfd\xdd\x33\xfd\xdd\x6f\xe8\xc7\x19\xc1\xb2\xf1\x99\x69\xa8\x54\x67\x69\x0d\xe9\xb2\x22\x1b\x0e\xb5\x71\xc7\xe1\xa4\xbf\x18\xf2\xd7\x15\xc3\xf9\x5f\x91\x4e\x0c\x08\x9a\xd6\x2a\x02\xc6\x55\x47\x75\xe1\x87\xbb\x51\x2c\x70\xae\x20\x7d\x13\xe5\xeb\x6b\xba\x81\x64\xfa\xe7\xd8\x6c\x7e\x04\x00\x00\xff\xff\x06\xe2\x8e\xc9\x43\x06\x00\x00") + +func layout_tpl_bytes() ([]byte, error) { + return bindata_read( + _layout_tpl, + "layout.tpl", + ) +} + +func layout_tpl() (*asset, error) { + bytes, err := layout_tpl_bytes() + if err != nil { + return nil, err + } + + info := bindata_file_info{name: "layout.tpl", size: 1603, mode: os.FileMode(438), modTime: time.Unix(1423332529, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _layoutemail_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xb2\x49\xcd\xb5\xab\xae\x2e\x49\xcd\x2d\xc8\x49\x2c\x49\x55\x50\x4a\x2c\x2d\xc9\x48\xca\x2f\x2e\x56\x52\xd0\xab\xad\xb5\xd1\x07\xca\x02\x02\x00\x00\xff\xff\x3a\xdb\x96\xd1\x22\x00\x00\x00") + +func layoutemail_tpl_bytes() ([]byte, error) { + return bindata_read( + _layoutemail_tpl, + "layoutEmail.tpl", + ) +} + +func layoutemail_tpl() (*asset, error) { + bytes, err := layoutemail_tpl_bytes() + if err != nil { + return nil, err + } + + info := bindata_file_info{name: "layoutEmail.tpl", size: 34, mode: os.FileMode(438), modTime: time.Unix(1423334931, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _login_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xcc\x94\x4b\x8e\xa3\x30\x10\x86\xf7\x23\xcd\x1d\x2c\xef\x09\x17\x00\xa4\x59\xcc\x6e\x46\x13\x4d\xba\x0f\x60\x4c\x11\xac\xd8\x2e\xcb\xd8\x79\x08\x71\xf7\x36\xcf\x06\xf2\xe8\x6d\x47\xb2\x54\xaa\xd4\xff\xa7\xfe\x2f\x86\xa4\x44\xab\x08\xe3\x4e\xa0\x4e\x69\x2c\xf1\x28\x34\x25\x0a\x5c\x85\x45\x4a\xf7\xff\x0e\x6f\x34\xfb\xf9\x83\x84\x4f\x52\x88\x33\xe1\x92\xd5\x75\x4a\x3b\x51\x74\xb4\xe8\x4d\xd3\x88\x92\xec\x7e\x5b\x8b\xb6\x6d\x49\xc5\xea\x08\xba\xba\x69\x40\x17\x6d\x3b\x69\xb7\x7a\xa1\x8d\x77\x83\xc1\x72\xa4\x1f\xab\x0d\xd3\x0f\xe6\x22\x56\x14\xa8\x69\x96\x88\x79\x09\x46\x4a\x16\xf9\x1a\x6c\xe8\xc6\x22\x9c\x4e\xba\xb5\xeb\x2d\x88\xbb\x19\x48\xa9\x83\xab\xa3\xab\x0c\x1c\xb5\xb3\x28\x29\xd1\x4c\x85\x81\xce\xac\xab\x28\x31\x92\x71\xa8\x50\x16\x60\x53\xfa\x3e\xb7\xcf\x4c\xfa\x30\xd7\x34\xbb\xa9\xb7\x09\x19\x87\x94\x13\xb0\x65\xfd\x3d\xe1\x49\xe4\xa7\xaf\xe1\x8d\xf4\x4c\x10\x5e\xd0\x16\x2f\x09\x7e\x0e\xad\x08\xee\xa7\xf6\x13\x56\x77\xcb\x57\x20\x4d\x94\x0f\xfb\x05\xda\x23\xa3\xd5\x96\x4b\xfd\x40\xf2\x50\xe1\xe5\x3f\x28\x50\x39\x84\xe1\x7b\xf2\xbc\x02\x7e\xca\xf1\xba\xda\x42\xb2\x1c\xe4\xab\x5b\x33\xab\xc6\x88\x56\xcd\xf7\xc0\x59\x0f\x34\x23\xd3\x6f\x92\xbf\xb0\x8c\xb7\x74\x5e\x2f\xdb\xff\xbf\xe3\x17\xb9\x77\x0e\xe7\xdc\xb9\xd3\x24\x9c\xc8\x58\xa1\x98\xbd\xf5\xf5\x80\x61\xdc\xa6\xf6\xb9\x12\x8e\x66\x7f\xba\x27\x35\x89\x07\xf5\x03\x0a\x1c\xcf\x0b\x08\x6c\xeb\x2f\x85\x3e\x3d\x35\x27\x95\x85\x32\xbc\x0d\xec\xe0\x42\xb3\xd1\x8e\xfc\xe2\x1c\xbd\x76\x49\xcc\xb6\x51\x92\xb8\xbb\x0d\xd9\x47\x00\x00\x00\xff\xff\x94\xfa\xa3\xd7\x4f\x04\x00\x00") func login_tpl_bytes() ([]byte, error) { return bindata_read( @@ -76,12 +116,12 @@ func login_tpl() (*asset, error) { return nil, err } - info := bindata_file_info{name: "login.tpl", size: 484, mode: os.FileMode(438), modTime: time.Unix(1421625609, 0)} + info := bindata_file_info{name: "login.tpl", size: 1103, mode: os.FileMode(438), modTime: time.Unix(1423332525, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _recover_complete_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00") +var _recover_complete_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xd4\x51\x41\xcf\x9b\x30\x0c\xbd\xf7\x57\x58\x51\xaf\x94\xfb\x04\x5c\xa6\x1d\xa7\x55\x6a\xff\x40\x20\xa6\x44\x0d\x49\x66\x42\xb7\x2a\xca\x7f\x5f\x20\xd0\x75\xa8\x95\x26\x4d\x3b\x7c\x48\x11\xb1\x63\x3f\xfb\xbd\x57\xb4\x86\x7a\xe0\x8d\x93\x46\x97\x2c\x27\x6c\xcc\x0d\x29\x6f\x4c\x6f\x15\x3a\x64\xd0\xa3\xeb\x8c\x28\xd9\xf1\xdb\xe9\xcc\xaa\x1d\xc4\xaf\x90\xda\x8e\x0e\xdc\xdd\x62\xc9\x3a\x29\x04\x6a\x06\x9a\xf7\x31\x72\xe6\x3a\x05\x37\xae\xc6\x18\x79\x7f\x38\x4f\x89\x10\x18\xe4\xa9\xd7\xfb\xbd\xe5\xc3\xf0\xc3\x90\xf8\x42\x34\xc0\xa7\x12\x0e\xf1\xf2\x95\xdb\xc3\x9a\x0f\x21\x4d\x11\xf2\x06\x8d\x8a\xc9\x92\x4d\x4b\x66\x17\x32\xa3\xf5\x5e\xb6\xf0\x07\x44\x08\xd0\xf1\x21\x43\x22\x43\xde\xa3\x8e\xfd\xcb\x9e\x5b\x94\x79\xed\x04\xf3\x54\x31\x57\x0d\x96\xeb\x17\x65\x19\x17\xc2\x68\x56\x15\xf2\xb1\x09\x87\x96\x67\xca\x34\xd7\x98\xcd\x65\x3c\x53\xeb\x06\x2d\xe9\xf3\xbc\x7b\x63\xb4\x23\xa3\xd8\x22\x9a\xc3\x9f\x6e\x95\x6c\xe5\xc2\xc0\x2a\xde\x60\x67\x94\x40\x8a\x7a\x3f\xd2\x84\xdf\x47\x49\x28\x56\x09\xe7\x11\x79\xe4\xf5\x3b\xf4\x9e\xb8\xbe\x20\xec\xa3\x0a\x93\xa4\x1b\x81\xde\x73\xed\x50\xd9\xac\x4e\x74\xbc\xb7\x24\xb5\x9b\x41\x42\xd8\x12\x5b\xa4\xdd\x3d\x4d\x5f\x0d\x8d\xe4\x5a\x49\xfd\xf1\x8d\xaf\x9b\xe7\xbf\xb0\xf7\x05\xe0\x47\x77\x79\x43\x69\x63\xf6\xe7\xf4\x0a\xff\x66\xfa\x4b\xd9\xfe\x93\xf7\xf3\xb5\x1e\x9d\x33\x0f\xbc\xda\x69\x88\x27\x8b\x48\x3d\xa7\xfb\x7c\x4f\xf0\x8b\x1e\xc3\x58\xf7\xd2\xb1\xea\x34\xff\x8b\x3c\xb5\x57\xbb\x22\x9f\xd4\xab\x7e\x05\x00\x00\xff\xff\x36\xd8\xe2\xc3\x8b\x04\x00\x00") func recover_complete_tpl_bytes() ([]byte, error) { return bindata_read( @@ -96,7 +136,7 @@ func recover_complete_tpl() (*asset, error) { return nil, err } - info := bindata_file_info{name: "recover-complete.tpl", size: 0, mode: os.FileMode(438), modTime: time.Unix(1421625609, 0)} + info := bindata_file_info{name: "recover-complete.tpl", size: 1163, mode: os.FileMode(438), modTime: time.Unix(1423332541, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -116,7 +156,7 @@ func recover_html_email() (*asset, error) { return nil, err } - info := bindata_file_info{name: "recover-html.email", size: 26, mode: os.FileMode(438), modTime: time.Unix(1422337208, 0)} + info := bindata_file_info{name: "recover-html.email", size: 26, mode: os.FileMode(438), modTime: time.Unix(1422773459, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -136,12 +176,12 @@ func recover_text_email() (*asset, error) { return nil, err } - info := bindata_file_info{name: "recover-text.email", size: 9, mode: os.FileMode(438), modTime: time.Unix(1421625609, 0)} + info := bindata_file_info{name: "recover-text.email", size: 9, mode: os.FileMode(438), modTime: time.Unix(1422773459, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _recover_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xca\xcf\x4d\xaf\x52\xc8\x54\x48\xcc\x55\x28\x4a\x4d\xce\x2f\x4b\x2d\xaa\x02\x04\x00\x00\xff\xff\x36\xaf\xf6\xd6\x12\x00\x00\x00") +var _recover_tpl = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xd4\x94\xc1\x6e\xa3\x30\x10\x86\xef\x79\x8a\x91\x95\x2b\xe1\xbe\x02\x2e\xd1\x1e\x57\xbb\xda\x36\x0f\x30\x80\x09\x56\x8c\x6d\x0d\x26\x6a\x64\xf9\xdd\x6b\x20\xa1\x89\x45\xa4\xf6\xd0\x43\x91\x90\x8c\x19\x7f\xe3\xff\xff\x2d\x67\x8d\xa6\x0e\xb0\xb2\x42\xab\x9c\xa5\xc4\x2b\x7d\xe6\xc4\xa0\xe3\xb6\xd5\x75\xce\xfe\xfd\x7d\x79\x65\xc5\x06\xc2\xe3\xdc\x76\xe8\x39\x29\xec\xf8\x6f\xa2\x1e\x7e\xe5\xb0\x0b\x83\x3f\x68\x76\xb7\x79\xef\xa7\xca\xac\x16\x67\xa8\x24\xf6\x7d\xce\x46\x7e\x72\x24\x3d\x18\xe7\x44\x03\x0f\x08\xef\xa1\xc5\x3e\xe1\x44\x9a\x9c\xe3\xaa\xf6\xfe\xda\x2b\xa6\x08\x65\x06\x3b\x63\xee\x2a\xa6\xaa\xde\xa0\x5a\x29\x4b\xb0\xae\xb5\x62\x45\x26\x96\x9d\x20\x34\x98\x8c\xfd\xc3\x6c\x2a\xc2\x3b\x2e\x8d\x68\x13\xe1\x61\xef\x95\x56\x96\xb4\x64\x60\x2f\x86\xe7\xcc\xf2\x37\xcb\x60\x54\x90\xb3\x9b\x16\x06\x46\x62\xc5\x5b\x2d\x6b\x4e\x39\x3b\x2c\xd3\x67\x94\x43\xa8\x73\x6e\x77\x58\x1c\x62\x90\xde\x69\x4c\x83\xc8\x8f\x4f\xe7\x08\xd5\x91\xc3\x36\x58\x32\xfa\x1b\xb9\xf5\x5c\x78\xcb\xa5\x49\x4a\xa9\xab\x13\x2b\x9c\x33\x24\x94\x9d\x20\xde\xc7\x2a\xaf\x3e\x6f\xee\xba\xdf\xd2\x0d\x4a\x1b\x41\xdd\xe1\x49\xc8\xd1\xef\x4f\x64\xbd\x02\xfc\xe9\x91\x47\x92\xa2\xe4\xf7\xf3\x5f\x58\x3b\x01\xfb\xd8\xbe\x2f\x1d\x84\x55\x2b\xbf\xe9\x3c\x8c\xc3\xf9\xbb\x1c\xac\xd5\x0b\xb4\xb4\x0a\xc2\x9b\x04\x5c\x87\x74\x99\xc6\x73\x8f\xab\x51\xfd\x50\x76\xc2\xb2\xe2\xff\x7c\x8b\x64\xe9\xbc\x7e\x26\x66\x18\x73\xa4\x50\xa7\xa7\x10\x68\x89\x37\xe1\x46\x92\xfa\x28\x42\xa6\x7b\x54\x15\x97\x59\x8a\xc5\x26\x4b\xc7\x90\x8a\xf7\x00\x00\x00\xff\xff\xc2\xb1\x79\x5b\xba\x04\x00\x00") func recover_tpl_bytes() ([]byte, error) { return bindata_read( @@ -156,7 +196,7 @@ func recover_tpl() (*asset, error) { return nil, err } - info := bindata_file_info{name: "recover.tpl", size: 18, mode: os.FileMode(438), modTime: time.Unix(1421625609, 0)} + info := bindata_file_info{name: "recover.tpl", size: 1210, mode: os.FileMode(438), modTime: time.Unix(1423332554, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -202,6 +242,8 @@ func AssetNames() []string { // _bindata is a table, holding each asset generator, mapped to its name. var _bindata = map[string]func() (*asset, error){ + "layout.tpl": layout_tpl, + "layoutEmail.tpl": layoutemail_tpl, "login.tpl": login_tpl, "recover-complete.tpl": recover_complete_tpl, "recover-html.email": recover_html_email, @@ -249,6 +291,10 @@ type _bintree_t struct { Children map[string]*_bintree_t } var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ + "layout.tpl": &_bintree_t{layout_tpl, map[string]*_bintree_t{ + }}, + "layoutEmail.tpl": &_bintree_t{layoutemail_tpl, map[string]*_bintree_t{ + }}, "login.tpl": &_bintree_t{login_tpl, map[string]*_bintree_t{ }}, "recover-complete.tpl": &_bintree_t{recover_complete_tpl, map[string]*_bintree_t{ diff --git a/internal/views/templates/layout.tpl b/internal/views/templates/layout.tpl new file mode 100644 index 0000000..77a403e --- /dev/null +++ b/internal/views/templates/layout.tpl @@ -0,0 +1,44 @@ + + + + + + + + +
+ {{if .FlashSuccess}} +
+
+
+ + {{print .FlashSuccess}} +
+
+
+ {{end}} + {{if .FlashError}} +
+
+
+ + {{print .FlashError}} +
+
+
+ {{end}} +
+
+
+
+
+ {{template "authboss" .}} +
+
+
+
+
+ + + + diff --git a/internal/views/templates/layoutEmail.tpl b/internal/views/templates/layoutEmail.tpl new file mode 100644 index 0000000..29d863c --- /dev/null +++ b/internal/views/templates/layoutEmail.tpl @@ -0,0 +1 @@ +{{template "authboss" .}} \ No newline at end of file diff --git a/internal/views/templates/login.tpl b/internal/views/templates/login.tpl index f9cb8be..1ab94eb 100644 --- a/internal/views/templates/login.tpl +++ b/internal/views/templates/login.tpl @@ -1,16 +1,26 @@ - - -
-

Log-in


-
- - - -
- - - - +
+
+ + +
+ {{.Error}} +
+ {{if .ShowRemember}} +
+ +
+ {{end}} + + {{if .ShowRecover}} + Recover Account + {{end}} + \ No newline at end of file diff --git a/internal/views/templates/recover-complete.tpl b/internal/views/templates/recover-complete.tpl index e69de29..d471703 100644 --- a/internal/views/templates/recover-complete.tpl +++ b/internal/views/templates/recover-complete.tpl @@ -0,0 +1,25 @@ +
+ + {{$passwordErrs := .ErrMap.password}} +
+
+ + +
+ {{range $err := $passwordErrs}} + {{print $err}} + {{end}} +
+ + {{$confirmPasswordErrs := .ErrMap.confirmPassword}} +
+
+ + +
+ {{range $err := $confirmPasswordErrs}} + {{print $err}} + {{end}} +
+ +
\ No newline at end of file diff --git a/internal/views/templates/recover.tpl b/internal/views/templates/recover.tpl index 098ca81..da6ad62 100644 --- a/internal/views/templates/recover.tpl +++ b/internal/views/templates/recover.tpl @@ -1 +1,26 @@ -omgz i am recoverz \ No newline at end of file +
+ {{$usernameErrs := .ErrMap.username}} +
+
+ + +
+ {{range $err := $usernameErrs}} + {{print $err}} + {{end}} +
+ + {{$confirmUsernameErrs := .ErrMap.confirmUsername}} +
+
+ + +
+ {{range $err := $confirmUsernameErrs}} + {{print $err}} + {{end}} +
+ + + Cancel +
\ No newline at end of file diff --git a/internal/views/views.go b/internal/views/views.go index dd35964..a4fe9e9 100644 --- a/internal/views/views.go +++ b/internal/views/views.go @@ -6,9 +6,9 @@ package views //go:generate go-bindata -pkg=views -prefix=templates templates import ( + "bytes" "errors" "html/template" - "io" "io/ioutil" "os" "path/filepath" @@ -24,13 +24,16 @@ type Templates map[string]*template.Template // ExecuteTemplate is a convenience wrapper for executing a template from the layout. Returns // ErrTemplateNotFound when the template is missing, othwerise error. -func (t Templates) ExecuteTemplate(w io.Writer, name string, data interface{}) error { +func (t Templates) ExecuteTemplate(name string, data interface{}) (buffer *bytes.Buffer, err error) { tpl, ok := t[name] if !ok { - return ErrTemplateNotFound + return nil, ErrTemplateNotFound } - return tpl.ExecuteTemplate(w, tpl.Name(), data) + buffer = &bytes.Buffer{} + err = tpl.ExecuteTemplate(buffer, tpl.Name(), data) + + return buffer, err } // Get parses all speicified files located in path. Each template is wrapped @@ -65,3 +68,13 @@ func Get(layout *template.Template, path string, files ...string) (Templates, er return m, nil } + +// Asset parses a specified file from the internal bindata. +func AssetToTemplate(file string) (*template.Template, error) { + b, err := Asset(file) + if err != nil { + return nil, err + } + + return template.New("").Parse(string(b)) +} diff --git a/internal/views/views_test.go b/internal/views/views_test.go index b936dbc..7012749 100644 --- a/internal/views/views_test.go +++ b/internal/views/views_test.go @@ -1,7 +1,6 @@ package views import ( - "bytes" "html/template" "io/ioutil" "os" @@ -15,8 +14,9 @@ func TestTemplates_ExecuteTemplate_ReturnsTemplateWhenFound(t *testing.T) { tpl, _ := template.New("").Parse("{{.Val}}") tpls := Templates{"a": tpl} - b := &bytes.Buffer{} - if err := tpls.ExecuteTemplate(b, "a", struct{ Val string }{"hi"}); err != nil { + //b := &bytes.Buffer{} + b, err := tpls.ExecuteTemplate("a", struct{ Val string }{"hi"}) + if err != nil { t.Error("Unexpected error:", err) } @@ -31,7 +31,7 @@ func TestTemplates_ExecuteTemplate_ReturnsErrTempalteNotFound(t *testing.T) { t.Parallel() tpls := Templates{} - err := tpls.ExecuteTemplate(ioutil.Discard, "shouldnotbefound", nil) + _, err := tpls.ExecuteTemplate("shouldnotbefound", nil) if err == nil { t.Error("Expected error") } @@ -65,8 +65,8 @@ func TestGet(t *testing.T) { t.Error("Unexpected error:", err) } - b := &bytes.Buffer{} - if err := tpls.ExecuteTemplate(b, filename, struct{ Val string }{"hi"}); err != nil { + b, err := tpls.ExecuteTemplate(filename, struct{ Val string }{"hi"}) + if err != nil { t.Error("Unexpected error:", err) } diff --git a/recover/recover.go b/recover/recover.go index c525708..3b7f528 100644 --- a/recover/recover.go +++ b/recover/recover.go @@ -1,17 +1,15 @@ package recover import ( - "bytes" "crypto/md5" "crypto/rand" "encoding/base64" "errors" "fmt" + "io" "net/http" "time" - "golang.org/x/crypto/bcrypt" - "gopkg.in/authboss.v0" "gopkg.in/authboss.v0/internal/flashutil" "gopkg.in/authboss.v0/internal/views" @@ -78,7 +76,7 @@ func (m *RecoverModule) Initialize(config *authboss.Config) (err error) { func (m *RecoverModule) Routes() authboss.RouteTable { return authboss.RouteTable{ "recover": m.recoverHandlerFunc, - "recover/complete": m.recoverCompleteHandlerFunc, + "recover/complete": nil, // TODO : Fix } } func (m *RecoverModule) Storage() authboss.StorageOptions { @@ -91,6 +89,20 @@ func (m *RecoverModule) Storage() authboss.StorageOptions { } } +func (m *RecoverModule) execTpl(tpl string, w http.ResponseWriter, page pageRecover) { + buffer, err := m.templates.ExecuteTemplate(tpl, page) + if err != nil { + fmt.Fprintf(m.config.LogWriter, errFormat, "unable to execute template", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + if _, err := io.Copy(w, buffer); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } +} + type pageRecover struct { Username, ConfirmUsername string ErrMap map[string][]string @@ -101,12 +113,18 @@ type pageRecover struct { func (m *RecoverModule) recoverHandlerFunc(ctx *authboss.Context, w http.ResponseWriter, r *http.Request) { switch r.Method { case methodGET: - m.execTpl(w, pageRecover{FlashError: flashutil.Pull(ctx.SessionStorer, authboss.FlashErrorKey)}) + page := pageRecover{} + page.FlashError = flashutil.Pull(ctx.SessionStorer, authboss.FlashErrorKey) + + m.execTpl(tplRecover, w, page) case methodPOST: - if page := m.recover(ctx); page != nil { - m.execTpl(w, page) + page, _ := m.recover(ctx) + + if page != nil { + m.execTpl(tplRecover, w, *page) return } + ctx.SessionStorer.Put(authboss.FlashSuccessKey, m.config.RecoverInitiateSuccessFlash) http.Redirect(w, r, m.config.RecoverRedirect, http.StatusFound) default: @@ -114,78 +132,91 @@ func (m *RecoverModule) recoverHandlerFunc(ctx *authboss.Context, w http.Respons } } -func (m *RecoverModule) execTpl(w http.ResponseWriter, data interface{}) { - if err := m.templates.ExecuteTemplate(w, tplRecover, data); err != nil { - fmt.Fprintf(m.config.LogWriter, errFormat, "unable to execute template", err) - } -} - -func (m *RecoverModule) recover(ctx *authboss.Context) *pageRecover { +func (m *RecoverModule) recover(ctx *authboss.Context) (errPage *pageRecover, emailSent <-chan struct{}) { username, _ := ctx.FirstPostFormValue("username") confirmUsername, _ := ctx.FirstPostFormValue("confirmUsername") policies := authboss.FilterValidators(m.config.Policies, "username") if validationErrs := ctx.Validate(policies, m.config.ConfirmFields...); len(validationErrs) > 0 { - return m.prepareRecoverPage(username, confirmUsername, "", "validation failed", validationErrs.Map()) + fmt.Fprintf(m.config.LogWriter, errFormat, "validation failed", validationErrs) + return &pageRecover{username, confirmUsername, validationErrs.Map(), "", ""}, nil } - if err := ctx.LoadUser(username, m.config.Storer); err != nil { - return m.prepareRecoverPage(username, confirmUsername, m.config.RecoverFailedErrorFlash, "failed to recover", nil) + err, emailSent := m.makeAndSendToken(ctx, username) + if err != nil { + fmt.Fprintf(m.config.LogWriter, errFormat, "failed to recover", err) + return &pageRecover{username, confirmUsername, nil, "", m.config.RecoverFailedErrorFlash}, nil + } + + return nil, emailSent +} + +func (m *RecoverModule) makeAndSendToken(ctx *authboss.Context, username string) (err error, emailSent <-chan struct{}) { + if err = ctx.LoadUser(username, m.config.Storer); err != nil { + return err, nil + } + + email, ok := ctx.User.String(attrEmail) + if !ok || email == "" { + return fmt.Errorf("email required: %v", attrEmail), nil } token := make([]byte, 32) - if _, err := rand.Read(token); err != nil { - return m.prepareRecoverPage(username, confirmUsername, m.config.RecoverFailedErrorFlash, "failed to recover", nil) + if _, err = rand.Read(token); err != nil { + return err, nil } sum := md5.Sum(token) ctx.User[attrRecoverToken] = base64.StdEncoding.EncodeToString(sum[:]) ctx.User[attrRecoverTokenExpiry] = time.Now().Add(m.config.RecoverTokenDuration) - if err := ctx.SaveUser(username, m.config.Storer); err != nil { - return m.prepareRecoverPage(username, confirmUsername, m.config.RecoverFailedErrorFlash, "failed to recover", nil) + if err = ctx.SaveUser(username, m.config.Storer); err != nil { + return err, nil } - /*if email, ok := ctx.User.String(attrEmail); !ok { - return m.prepareRecoverPage(username, confirmUsername, m.config.RecoverFailedErrorFlash, "failed to recover", nil) - } else { - go m.sendRecoverEmail(email, token) - }*/ - - return nil + return nil, m.sendRecoverEmail(email, token) } -func (m *RecoverModule) prepareRecoverPage(username, confirmUsername, flashError, message string, validationErrs map[string][]string) *pageRecover { - fmt.Fprintf(m.config.LogWriter, errFormat, message, validationErrs) - return &pageRecover{username, confirmUsername, validationErrs, "", flashError} +func (m *RecoverModule) sendRecoverEmail(to string, token []byte) <-chan struct{} { + emailSent := make(chan struct{}, 1) + + go func() { + data := struct{ Link string }{fmt.Sprintf("%s/recover/complete?token=%s", m.config.HostName, base64.URLEncoding.EncodeToString(token))} + + htmlEmailBody, err := m.emailTemplates.ExecuteTemplate(tplInitHTMLEmail, data) + if err != nil { + fmt.Fprintf(m.config.LogWriter, errFormat, "failed to build html email", err) + close(emailSent) + return + } + + textEmaiLBody, err := m.emailTemplates.ExecuteTemplate(tplInitTextEmail, data) + if err != nil { + fmt.Fprintf(m.config.LogWriter, errFormat, "failed to build plaintext email", err) + close(emailSent) + return + } + + if err := m.config.Mailer.Send(authboss.Email{ + To: []string{to}, + ToNames: []string{""}, + From: m.config.EmailFrom, + Subject: m.config.EmailSubjectPrefix + "Password Reset", + TextBody: textEmaiLBody.String(), + HTMLBody: htmlEmailBody.String(), + }); err != nil { + fmt.Fprintf(m.config.LogWriter, errFormat, "failed to send email", err) + close(emailSent) + return + } + + emailSent <- struct{}{} + }() + + return emailSent } -func (m *RecoverModule) sendRecoverEmail(to string, token []byte) { - data := struct{ Link string }{fmt.Sprintf("%s/recover/complete?token=%s", m.config.HostName, base64.URLEncoding.EncodeToString(token))} - - htmlEmailBody := &bytes.Buffer{} - if err := m.emailTemplates.ExecuteTemplate(htmlEmailBody, tplInitHTMLEmail, data); err != nil { - fmt.Fprintf(m.config.LogWriter, errFormat, "failed to build html tpl", err) - } - - textEmailBody := &bytes.Buffer{} - if err := m.emailTemplates.ExecuteTemplate(textEmaiLBody, tplInitTextEmail, data); err != nil { - fmt.Fprintf(m.config.LogWriter, errFormat, "failed to build plaintext tpl", err) - } - - if err := m.config.Mailer.Send(authboss.Email{ - To: []string{to}, - ToNames: []string{""}, - From: m.config.EmailFrom, - Subject: m.config.EmailSubjectPrefix + "Password Reset", - TextBody: textEmailBody.String(), - HTMLBody: htmlEmailBody.String(), - }); err != nil { - fmt.Fprintf(m.config.LogWriter, errFormat, "failed to send email", err) - } -} - -type pageRecoverComplete struct { +/*type pageRecoverComplete struct { Token string ErrMap map[string][]string FlashSuccess string @@ -295,3 +326,4 @@ func (m *RecoverModule) verifyToken(token string) (attrs authboss.Attributes, er return authboss.Unbind(userInter), nil } +*/ diff --git a/recover/recover_test.go b/recover/recover_test.go index 4b9b9f3..2bcea4c 100644 --- a/recover/recover_test.go +++ b/recover/recover_test.go @@ -2,6 +2,8 @@ package recover import ( "bytes" + "encoding/base64" + "errors" "fmt" "html/template" "io" @@ -16,37 +18,9 @@ import ( "gopkg.in/authboss.v0" "gopkg.in/authboss.v0/internal/mocks" + "gopkg.in/authboss.v0/internal/views" ) -var filenames = []string{ - tplLogin, - tplRecover, - tplRecoverComplete, - tplInitHTMLEmail, - tplInitTextEmail, -} - -func TestMain(main *testing.M) { - for _, filename := range filenames { - file, err := os.Create(fmt.Sprintf("%s/%s", os.TempDir(), filename)) - if err != nil { - panic(err) - } - - if _, err := file.WriteString(filename); err != nil { - - } - } - - code := main.Run() - - for _, filename := range filenames { - os.Remove(filename) - } - - os.Exit(code) -} - type failStorer int func (_ failStorer) Create(_ string, _ authboss.Attributes) error { return nil } @@ -105,16 +79,21 @@ func testValidTestConfig() *authboss.Config { config := &authboss.Config{} config.Storer = mocks.NewMockStorer() - config.ViewsPath = os.TempDir() + config.EmailFrom = "auth@boss.com" var err error - if config.Layout, err = template.New("").Parse(`{{template "authboss" .}}`); err != nil { + if config.Layout, err = views.AssetToTemplate("layout.tpl"); err != nil { panic(err) } - if config.LayoutEmail, _ = template.New("").Parse(`{{template "authboss" .}}`); err != nil { + 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", @@ -122,21 +101,23 @@ func testValidTestConfig() *authboss.Config { }, } 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() - logger := &bytes.Buffer{} - c.LogWriter = logger m := &RecoverModule{} if err := m.Initialize(c); err != nil { panic(err) } - return m, logger + return m, c.LogWriter.(*bytes.Buffer) } func Test_Routes(t *testing.T) { @@ -212,39 +193,159 @@ func Test_recoverHandlerFunc_GET(t *testing.T) { 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 w.Body.String() != "recover.tpl" { + if !bytes.Equal(expectedBody.Bytes(), w.Body.Bytes()) { t.Error("Unexpected body:", w.Body.String()) } } -/*func TestRecoverModule_recoverHandlerFunc_POST(t *testing.T) { +func Test_recoverHandlerFunc_POST_RecoveryFailed(t *testing.T) { t.Parallel() -}*/ -func Test_recover(t *testing.T) { + 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() - page := m.recover(mocks.MockRequestContext()) + 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]: map[username:[Cannot be blank]]\n") + 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.Error("Unexpected logs:", string(expectedLog)) + t.Errorf("Unexpected logs: %q", string(actualLog)) } + if emailSent != nil { + t.Error("Unexpected sent email") + } +} - page = m.recover(mocks.MockRequestContext("username", "a", "confirmUsername", "b")) +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") } @@ -254,22 +355,128 @@ func Test_recover(t *testing.T) { 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]: map[confirmUsername:[Does not match username]]\n") - actualLog, err = ioutil.ReadAll(logger) + 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(expectedLog)) + 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"} + storer.Users["a"] = authboss.Attributes{"username": "", "password": "", "email": "a@b.c"} - page = m.recover(mocks.MockRequestContext("username", "a", "confirmUsername", "a")) + 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 { @@ -281,6 +488,127 @@ func Test_recover(t *testing.T) { 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) {