8 changed files with 642 additions and 13 deletions
@ -0,0 +1,90 @@ |
|||
package auth |
|||
|
|||
import ( |
|||
"errors" |
|||
"testing" |
|||
) |
|||
|
|||
func TestUserManagementLifecycle(t *testing.T) { |
|||
store := openTestStore(t, t.TempDir()) |
|||
|
|||
created, err := store.CreateUser("reader@example.com", "Temporary-1!", RoleUser) |
|||
if err != nil { |
|||
t.Fatalf("CreateUser() error = %v", err) |
|||
} |
|||
if created.Role != RoleUser || !created.MustChangePassword || created.Disabled { |
|||
t.Errorf("CreateUser() = %#v, want enabled user role with forced password change", created) |
|||
} |
|||
|
|||
user, err := store.Authenticate(created.Email, "Temporary-1!") |
|||
if err != nil { |
|||
t.Fatalf("Authenticate(created user) error = %v", err) |
|||
} |
|||
if !user.MustChangePassword { |
|||
t.Error("created user's temporary password does not require a change") |
|||
} |
|||
token, err := store.CreateSession(user.ID) |
|||
if err != nil { |
|||
t.Fatalf("CreateSession() error = %v", err) |
|||
} |
|||
|
|||
if err := store.ResetPassword(created.ID, "Replacement-2!"); err != nil { |
|||
t.Fatalf("ResetPassword() error = %v", err) |
|||
} |
|||
if _, err := store.LookupSession(token); !errors.Is(err, ErrSessionNotFound) { |
|||
t.Errorf("LookupSession() after reset error = %v, want ErrSessionNotFound", err) |
|||
} |
|||
if _, err := store.Authenticate(created.Email, "Temporary-1!"); !errors.Is(err, ErrInvalidCredentials) { |
|||
t.Errorf("Authenticate(old password) error = %v, want ErrInvalidCredentials", err) |
|||
} |
|||
user, err = store.Authenticate(created.Email, "Replacement-2!") |
|||
if err != nil { |
|||
t.Fatalf("Authenticate(reset password) error = %v", err) |
|||
} |
|||
if !user.MustChangePassword { |
|||
t.Error("reset password does not require a change") |
|||
} |
|||
token, err = store.CreateSession(user.ID) |
|||
if err != nil { |
|||
t.Fatalf("CreateSession() after reset error = %v", err) |
|||
} |
|||
|
|||
if err := store.DisableUser(created.ID); err != nil { |
|||
t.Fatalf("DisableUser() error = %v", err) |
|||
} |
|||
if _, err := store.Authenticate(created.Email, "Replacement-2!"); !errors.Is(err, ErrInvalidCredentials) { |
|||
t.Errorf("Authenticate(disabled user) error = %v, want ErrInvalidCredentials", err) |
|||
} |
|||
if _, err := store.LookupSession(token); !errors.Is(err, ErrSessionNotFound) { |
|||
t.Errorf("LookupSession() for disabled user error = %v, want ErrSessionNotFound", err) |
|||
} |
|||
|
|||
users, err := store.ListUsers() |
|||
if err != nil { |
|||
t.Fatalf("ListUsers() error = %v", err) |
|||
} |
|||
managed := userByEmail(users, created.Email) |
|||
if managed == nil || !managed.Disabled { |
|||
t.Errorf("ListUsers() did not return %q as disabled: %#v", created.Email, users) |
|||
} |
|||
|
|||
if err := store.DeleteUser(created.ID); err != nil { |
|||
t.Fatalf("DeleteUser() error = %v", err) |
|||
} |
|||
users, err = store.ListUsers() |
|||
if err != nil { |
|||
t.Fatalf("ListUsers() after deletion error = %v", err) |
|||
} |
|||
if userByEmail(users, created.Email) != nil { |
|||
t.Errorf("ListUsers() still returned deleted user %q", created.Email) |
|||
} |
|||
} |
|||
|
|||
func userByEmail(users []User, email string) *User { |
|||
for index := range users { |
|||
if users[index].Email == email { |
|||
return &users[index] |
|||
} |
|||
} |
|||
return nil |
|||
} |
|||
@ -0,0 +1,149 @@ |
|||
package web |
|||
|
|||
import ( |
|||
"errors" |
|||
"net/http" |
|||
"net/http/httptest" |
|||
"net/url" |
|||
"strconv" |
|||
"strings" |
|||
"testing" |
|||
|
|||
"webfilebrowser/internal/auth" |
|||
) |
|||
|
|||
func TestNonAdminUsersAreForbiddenFromEveryAdminRoute(t *testing.T) { |
|||
store, handler := newTestApplication(t) |
|||
created, err := store.CreateUser("reader@example.com", "Temporary-1!", auth.RoleUser) |
|||
if err != nil { |
|||
t.Fatalf("CreateUser() error = %v", err) |
|||
} |
|||
userCookie := completeUserPasswordChange(t, handler, created.Email, "Temporary-1!", "Reader-password-2!") |
|||
|
|||
for _, target := range []struct { |
|||
method string |
|||
path string |
|||
}{ |
|||
{method: http.MethodGet, path: "/admin"}, |
|||
{method: http.MethodGet, path: adminUsersPath}, |
|||
{method: http.MethodPost, path: adminCreateUserPath}, |
|||
{method: http.MethodPost, path: adminResetUserPath}, |
|||
{method: http.MethodPost, path: adminDisableUserPath}, |
|||
{method: http.MethodPost, path: adminDeleteUserPath}, |
|||
} { |
|||
t.Run(target.method+" "+target.path, func(t *testing.T) { |
|||
response := postOrGetWithCookie(handler, target.method, target.path, userCookie) |
|||
if response.Code != http.StatusForbidden { |
|||
t.Errorf("%s %s status = %d, want %d", target.method, target.path, response.Code, http.StatusForbidden) |
|||
} |
|||
if response.Header().Get("Location") != "" { |
|||
t.Errorf("%s %s redirected to %q, want no redirect", target.method, target.path, response.Header().Get("Location")) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
func TestAdminCanManageUsersAndTemporaryPasswordsRequireChange(t *testing.T) { |
|||
store, handler := newTestApplication(t) |
|||
adminCookie := completeSeedPasswordChange(t, handler) |
|||
|
|||
usersResponse := getWithCookie(handler, adminUsersPath, adminCookie) |
|||
if usersResponse.Code != http.StatusOK || !strings.Contains(usersResponse.Body.String(), "Create user") { |
|||
t.Fatalf("GET %s = (%d, %q), want admin management page", adminUsersPath, usersResponse.Code, usersResponse.Body.String()) |
|||
} |
|||
|
|||
createResponse := postForm(handler, adminCreateUserPath, url.Values{ |
|||
"email": {"new-reader@example.com"}, |
|||
"temporary_password": {"Temporary-1!"}, |
|||
"role": {string(auth.RoleUser)}, |
|||
}, adminCookie) |
|||
if createResponse.Code != http.StatusSeeOther || createResponse.Header().Get("Location") != adminUsersPath { |
|||
t.Fatalf("POST %s = (%d, %q), want redirect to %s", adminCreateUserPath, createResponse.Code, createResponse.Header().Get("Location"), adminUsersPath) |
|||
} |
|||
|
|||
created := managedUser(t, store, "new-reader@example.com") |
|||
loginResponse := postForm(handler, loginPath, url.Values{ |
|||
"email": {created.Email}, |
|||
"password": {"Temporary-1!"}, |
|||
}, nil) |
|||
if loginResponse.Code != http.StatusSeeOther || loginResponse.Header().Get("Location") != changePasswordPath { |
|||
t.Fatalf("new user login = (%d, %q), want redirect to %s", loginResponse.Code, loginResponse.Header().Get("Location"), changePasswordPath) |
|||
} |
|||
|
|||
resetResponse := postForm(handler, adminResetUserPath, url.Values{ |
|||
"id": {strconv.FormatInt(created.ID, 10)}, |
|||
"temporary_password": {"Replacement-2!"}, |
|||
}, adminCookie) |
|||
if resetResponse.Code != http.StatusSeeOther || resetResponse.Header().Get("Location") != adminUsersPath { |
|||
t.Fatalf("POST %s = (%d, %q), want redirect to %s", adminResetUserPath, resetResponse.Code, resetResponse.Header().Get("Location"), adminUsersPath) |
|||
} |
|||
if _, err := store.Authenticate(created.Email, "Replacement-2!"); err != nil { |
|||
t.Fatalf("Authenticate(reset user) error = %v", err) |
|||
} |
|||
|
|||
disableResponse := postForm(handler, adminDisableUserPath, url.Values{"id": {strconv.FormatInt(created.ID, 10)}}, adminCookie) |
|||
if disableResponse.Code != http.StatusSeeOther || disableResponse.Header().Get("Location") != adminUsersPath { |
|||
t.Fatalf("POST %s = (%d, %q), want redirect to %s", adminDisableUserPath, disableResponse.Code, disableResponse.Header().Get("Location"), adminUsersPath) |
|||
} |
|||
if _, err := store.Authenticate(created.Email, "Replacement-2!"); !errors.Is(err, auth.ErrInvalidCredentials) { |
|||
t.Errorf("Authenticate(disabled user) error = %v, want invalid credentials", err) |
|||
} |
|||
|
|||
deleteResponse := postForm(handler, adminDeleteUserPath, url.Values{"id": {strconv.FormatInt(created.ID, 10)}}, adminCookie) |
|||
if deleteResponse.Code != http.StatusSeeOther || deleteResponse.Header().Get("Location") != adminUsersPath { |
|||
t.Fatalf("POST %s = (%d, %q), want redirect to %s", adminDeleteUserPath, deleteResponse.Code, deleteResponse.Header().Get("Location"), adminUsersPath) |
|||
} |
|||
if got := managedUserOrNil(t, store, created.Email); got != nil { |
|||
t.Errorf("deleted user remains in management list: %#v", got) |
|||
} |
|||
} |
|||
|
|||
func completeUserPasswordChange(t *testing.T, handler http.Handler, email, temporaryPassword, newPassword string) *http.Cookie { |
|||
t.Helper() |
|||
loginResponse := postForm(handler, loginPath, url.Values{ |
|||
"email": {email}, |
|||
"password": {temporaryPassword}, |
|||
}, nil) |
|||
if loginResponse.Code != http.StatusSeeOther || loginResponse.Header().Get("Location") != changePasswordPath { |
|||
t.Fatalf("temporary user login = (%d, %q), want redirect to %s", loginResponse.Code, loginResponse.Header().Get("Location"), changePasswordPath) |
|||
} |
|||
cookie := responseCookie(t, loginResponse, sessionCookieName) |
|||
changeResponse := postForm(handler, changePasswordPath, url.Values{ |
|||
"current_password": {temporaryPassword}, |
|||
"new_password": {newPassword}, |
|||
}, cookie) |
|||
if changeResponse.Code != http.StatusSeeOther { |
|||
t.Fatalf("POST %s = %d, want %d", changePasswordPath, changeResponse.Code, http.StatusSeeOther) |
|||
} |
|||
return cookie |
|||
} |
|||
|
|||
func postOrGetWithCookie(handler http.Handler, method, target string, cookie *http.Cookie) *httptest.ResponseRecorder { |
|||
if method == http.MethodGet { |
|||
return getWithCookie(handler, target, cookie) |
|||
} |
|||
return postForm(handler, target, url.Values{}, cookie) |
|||
} |
|||
|
|||
func managedUser(t *testing.T, store *auth.Store, email string) auth.User { |
|||
t.Helper() |
|||
user := managedUserOrNil(t, store, email) |
|||
if user == nil { |
|||
t.Fatalf("ListUsers() did not return %q", email) |
|||
} |
|||
return *user |
|||
} |
|||
|
|||
func managedUserOrNil(t *testing.T, store *auth.Store, email string) *auth.User { |
|||
t.Helper() |
|||
users, err := store.ListUsers() |
|||
if err != nil { |
|||
t.Fatalf("ListUsers() error = %v", err) |
|||
} |
|||
for index := range users { |
|||
if users[index].Email == email { |
|||
return &users[index] |
|||
} |
|||
} |
|||
return nil |
|||
} |
|||
Loading…
Reference in new issue