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 }