859 lines
29 KiB
Go
859 lines
29 KiB
Go
package server
|
|
|
|
import (
|
|
"html/template"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"warpbox/lib/boxstore"
|
|
"warpbox/lib/config"
|
|
"warpbox/lib/metastore"
|
|
)
|
|
|
|
func TestAccountLoginSuccess(t *testing.T) {
|
|
app, _ := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
|
|
response := postAccountLogin(router, "admin", "secret")
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected login redirect, got %d", response.Code)
|
|
}
|
|
if location := response.Header().Get("Location"); location != "/account" {
|
|
t.Fatalf("expected redirect to /account, got %q", location)
|
|
}
|
|
if cookie := findResponseCookie(response, accountSessionCookie); cookie == nil || cookie.Value == "" {
|
|
t.Fatal("expected account session cookie")
|
|
}
|
|
}
|
|
|
|
func TestAccountLoginFailure(t *testing.T) {
|
|
app, _ := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
|
|
response := postAccountLogin(router, "admin", "wrong")
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected failed login to render form, got %d", response.Code)
|
|
}
|
|
if cookie := findResponseCookie(response, accountSessionCookie); cookie != nil {
|
|
t.Fatal("did not expect account session cookie")
|
|
}
|
|
if !strings.Contains(response.Body.String(), "not accepted") {
|
|
t.Fatal("expected login failure message")
|
|
}
|
|
}
|
|
|
|
func TestAccountDisabledUserLoginFailure(t *testing.T) {
|
|
app, user := setupAccountTestApp(t)
|
|
user.Disabled = true
|
|
if err := app.store.UpdateUser(user); err != nil {
|
|
t.Fatalf("UpdateUser returned error: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
|
|
response := postAccountLogin(router, "admin", "secret")
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected disabled login to render form, got %d", response.Code)
|
|
}
|
|
if cookie := findResponseCookie(response, accountSessionCookie); cookie != nil {
|
|
t.Fatal("did not expect account session cookie")
|
|
}
|
|
if !strings.Contains(response.Body.String(), "not accepted") {
|
|
t.Fatal("expected login failure message")
|
|
}
|
|
}
|
|
|
|
func TestAccountLogoutRequiresCSRF(t *testing.T) {
|
|
app, user := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(user.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/logout", nil)
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusForbidden {
|
|
t.Fatalf("expected missing CSRF token to be forbidden, got %d", response.Code)
|
|
}
|
|
}
|
|
|
|
func TestAccountDashboardRequiresAuth(t *testing.T) {
|
|
app, _ := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/account", nil)
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected dashboard redirect, got %d", response.Code)
|
|
}
|
|
if location := response.Header().Get("Location"); location != "/account/login" {
|
|
t.Fatalf("expected redirect to /account/login, got %q", location)
|
|
}
|
|
}
|
|
|
|
func TestAccountDashboardLoadsForBootstrapAdmin(t *testing.T) {
|
|
app, user := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(user.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/account", nil)
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected dashboard to load, got %d", response.Code)
|
|
}
|
|
body := response.Body.String()
|
|
for _, text := range []string{"Dashboard", "Recent Boxes", "Users"} {
|
|
if !strings.Contains(body, text) {
|
|
t.Fatalf("expected dashboard body to contain %q", text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAccountDashboardHidesAdminOnlyLinksForRegularUser(t *testing.T) {
|
|
app, _ := setupAccountTestApp(t)
|
|
user, err := app.store.CreateUserWithPassword("maya", "maya@example.test", "secret", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword returned error: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(user.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/account", nil)
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected dashboard to load, got %d", response.Code)
|
|
}
|
|
body := response.Body.String()
|
|
for _, text := range []string{">Users<", ">Settings<"} {
|
|
if strings.Contains(body, text) {
|
|
t.Fatalf("expected dashboard body to hide %q", text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAdminEntryRedirectsToAccount(t *testing.T) {
|
|
app, _ := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
|
|
cases := map[string]string{
|
|
"/admin/login": "/account/login",
|
|
"/admin": "/account",
|
|
}
|
|
for path, wantLocation := range cases {
|
|
request := httptest.NewRequest(http.MethodGet, path, nil)
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected %s redirect, got %d", path, response.Code)
|
|
}
|
|
if location := response.Header().Get("Location"); location != wantLocation {
|
|
t.Fatalf("expected %s to redirect to %s, got %q", path, wantLocation, location)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setupAccountTestApp(t *testing.T) (*App, metastore.User) {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
restoreUploadRoot := boxstore.UploadRoot()
|
|
t.Cleanup(func() { boxstore.SetUploadRoot(restoreUploadRoot) })
|
|
boxstore.SetUploadRoot(t.TempDir())
|
|
|
|
store, err := metastore.Open(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("Open returned error: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = store.Close() })
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
cfg.AdminUsername = "admin"
|
|
cfg.AdminPassword = "secret"
|
|
cfg.AdminEmail = "admin@example.test"
|
|
cfg.AdminEnabled = config.AdminEnabledAuto
|
|
cfg.SessionTTLSeconds = 3600
|
|
bootstrap, err := metastore.BootstrapAdmin(cfg, store)
|
|
if err != nil {
|
|
t.Fatalf("BootstrapAdmin returned error: %v", err)
|
|
}
|
|
if bootstrap.AdminUser == nil {
|
|
t.Fatal("expected bootstrap admin user")
|
|
}
|
|
|
|
app := &App{
|
|
config: cfg,
|
|
store: store,
|
|
adminLoginEnabled: bootstrap.AdminLoginEnabled,
|
|
}
|
|
return app, *bootstrap.AdminUser
|
|
}
|
|
|
|
func setupAccountTestRouter(t *testing.T, app *App) *gin.Engine {
|
|
t.Helper()
|
|
router := gin.New()
|
|
templates, err := template.ParseGlob(filepath.Join("..", "..", "templates", "*.html"))
|
|
if err != nil {
|
|
t.Fatalf("ParseGlob returned error: %v", err)
|
|
}
|
|
router.SetHTMLTemplate(templates)
|
|
app.registerAccountRoutes(router)
|
|
app.registerAdminRoutes(router)
|
|
return router
|
|
}
|
|
|
|
func postAccountLogin(router *gin.Engine, username string, password string) *httptest.ResponseRecorder {
|
|
form := url.Values{}
|
|
form.Set("username", username)
|
|
form.Set("password", password)
|
|
request := httptest.NewRequest(http.MethodPost, "/account/login", strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
return response
|
|
}
|
|
|
|
func findResponseCookie(response *httptest.ResponseRecorder, name string) *http.Cookie {
|
|
for _, cookie := range response.Result().Cookies() {
|
|
if cookie.Name == name {
|
|
return cookie
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestUsersPagePermissionDeniedForNoPerms(t *testing.T) {
|
|
app, _ := setupAccountTestApp(t)
|
|
user, err := app.store.CreateUserWithPassword("viewer", "viewer@example.test", "secret", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword returned error: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(user.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/account/users", nil)
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusForbidden {
|
|
t.Fatalf("expected permission denied, got %d", response.Code)
|
|
}
|
|
if !strings.Contains(response.Body.String(), "Permission denied") {
|
|
t.Fatal("expected permission denied message")
|
|
}
|
|
}
|
|
|
|
func TestUsersPageLoadsForAdmin(t *testing.T) {
|
|
app, user := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(user.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/account/users", nil)
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected users page to load, got %d", response.Code)
|
|
}
|
|
body := response.Body.String()
|
|
for _, text := range []string{"WarpBox Users", "Create or Invite", "Total users"} {
|
|
if !strings.Contains(body, text) {
|
|
t.Fatalf("expected users page body to contain %q", text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUsersPageListFilters(t *testing.T) {
|
|
app, user := setupAccountTestApp(t)
|
|
_, err := app.store.CreateUserWithPassword("beta", "beta@example.test", "secret", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword returned error: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(user.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/account/users?q=beta", nil)
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected users page to load, got %d", response.Code)
|
|
}
|
|
body := response.Body.String()
|
|
if !strings.Contains(body, "beta") {
|
|
t.Fatal("expected filtered list to contain beta")
|
|
}
|
|
if !strings.Contains(body, "1 matching user(s)") {
|
|
t.Fatalf("expected 1 matching user for beta filter, got body: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestUserCreation(t *testing.T) {
|
|
app, user := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(user.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
form.Set("action", "create")
|
|
form.Set("mode", "create")
|
|
form.Set("username", "newuser")
|
|
form.Set("email", "new@example.test")
|
|
form.Set("password", "password123")
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users", strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect after create, got %d", response.Code)
|
|
}
|
|
|
|
created, ok, err := app.store.GetUserByUsername("newuser")
|
|
if err != nil || !ok {
|
|
t.Fatal("expected newuser to exist")
|
|
}
|
|
if created.Disabled {
|
|
t.Fatal("expected newuser to be active")
|
|
}
|
|
}
|
|
|
|
func TestUserInviteCreation(t *testing.T) {
|
|
app, user := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(user.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
form.Set("action", "create")
|
|
form.Set("mode", "invite")
|
|
form.Set("username", "invited")
|
|
form.Set("email", "invited@example.test")
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users", strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect after invite, got %d", response.Code)
|
|
}
|
|
|
|
created, ok, err := app.store.GetUserByUsername("invited")
|
|
if err != nil || !ok {
|
|
t.Fatal("expected invited user to exist")
|
|
}
|
|
if !created.Disabled {
|
|
t.Fatal("expected invited user to be disabled")
|
|
}
|
|
if !strings.HasPrefix(created.PasswordHash, "invite/") {
|
|
t.Fatal("expected invited user to have invite prefix")
|
|
}
|
|
}
|
|
|
|
func TestBulkDisableRejectsSelf(t *testing.T) {
|
|
app, user := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(user.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
form.Set("selected_ids", user.ID)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users/bulk/disable", strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect, got %d", response.Code)
|
|
}
|
|
location := response.Header().Get("Location")
|
|
if !strings.Contains(location, "cannot disable yourself") && !strings.Contains(location, "error=") {
|
|
t.Fatalf("expected self-disable rejection, got location %q", location)
|
|
}
|
|
}
|
|
|
|
func TestBulkDisableProtectsFinalAdmin(t *testing.T) {
|
|
app, user := setupAccountTestApp(t)
|
|
adminTag, ok, err := app.store.GetTagByName(metastore.AdminTagName)
|
|
if err != nil || !ok || adminTag.ID == "" {
|
|
t.Fatal("expected admin tag")
|
|
}
|
|
second, err := app.store.CreateUserWithPassword("admin2", "admin2@example.test", "secret", []string{adminTag.ID})
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword returned error: %v", err)
|
|
}
|
|
|
|
// Admin tries to disable the other admin (not self): should work since self remains.
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(user.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
form.Set("selected_ids", second.ID)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users/bulk/disable", strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected success redirect, got %d", response.Code)
|
|
}
|
|
location := response.Header().Get("Location")
|
|
if !strings.Contains(location, "user(s) disabled") {
|
|
t.Fatalf("expected success message, got %q", location)
|
|
}
|
|
|
|
// Verify admin2 is disabled, admin1 still active
|
|
disabledUser, ok, _ := app.store.GetUserByUsername("admin2")
|
|
if !ok || !disabledUser.Disabled {
|
|
t.Fatal("expected admin2 to be disabled")
|
|
}
|
|
adminUser, ok, _ := app.store.GetUserByUsername("admin")
|
|
if !ok || adminUser.Disabled {
|
|
t.Fatal("expected admin to remain active")
|
|
}
|
|
|
|
// Now try to disable the only remaining admin (self): should be rejected
|
|
form2 := url.Values{}
|
|
form2.Set("csrf_token", session.CSRFToken)
|
|
form2.Set("selected_ids", user.ID)
|
|
|
|
req2 := httptest.NewRequest(http.MethodPost, "/account/users/bulk/disable", strings.NewReader(form2.Encode()))
|
|
req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req2.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
resp2 := httptest.NewRecorder()
|
|
router.ServeHTTP(resp2, req2)
|
|
|
|
if resp2.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect for self-disable rejection, got %d", resp2.Code)
|
|
}
|
|
loc2 := resp2.Header().Get("Location")
|
|
if !strings.Contains(loc2, "cannot disable yourself") {
|
|
t.Fatalf("expected self-disable rejection, got %q", loc2)
|
|
}
|
|
}
|
|
|
|
func TestUserEditPagePermissionDenied(t *testing.T) {
|
|
app, _ := setupAccountTestApp(t)
|
|
regular, err := app.store.CreateUserWithPassword("viewer2", "viewer2@example.test", "secret", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(regular.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/account/users/"+regular.ID, nil)
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403, got %d", response.Code)
|
|
}
|
|
}
|
|
|
|
func TestUserEditPageLoadsForAdmin(t *testing.T) {
|
|
app, admin := setupAccountTestApp(t)
|
|
target, err := app.store.CreateUserWithPassword("edittarget", "edittarget@example.test", "secret", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(admin.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/account/users/"+target.ID, nil)
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", response.Code)
|
|
}
|
|
body := response.Body.String()
|
|
for _, text := range []string{"edittarget", "Access rights", "Limits", "Setting overrides", "Resolved policy"} {
|
|
if !strings.Contains(body, text) {
|
|
t.Fatalf("expected body to contain %q", text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUserEditProfileUpdate(t *testing.T) {
|
|
app, admin := setupAccountTestApp(t)
|
|
target, err := app.store.CreateUserWithPassword("origname", "orig@example.test", "secret", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(admin.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
form.Set("username", "newname")
|
|
form.Set("email", "new@example.test")
|
|
form.Set("admin_note", "test note")
|
|
form.Set("state", "active")
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users/"+target.ID, strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect, got %d", response.Code)
|
|
}
|
|
|
|
updated, ok, _ := app.store.GetUser(target.ID)
|
|
if !ok {
|
|
t.Fatal("user not found after update")
|
|
}
|
|
if updated.Username != "newname" {
|
|
t.Fatalf("expected username newname, got %q", updated.Username)
|
|
}
|
|
if updated.AdminNote != "test note" {
|
|
t.Fatalf("expected admin note, got %q", updated.AdminNote)
|
|
}
|
|
}
|
|
|
|
func TestUserEditAccessRightsUpdate(t *testing.T) {
|
|
app, admin := setupAccountTestApp(t)
|
|
target, err := app.store.CreateUserWithPassword("perm_target", "perm@example.test", "secret", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(admin.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
form.Set("username", target.Username)
|
|
form.Set("email", target.Email)
|
|
form.Set("upload_allowed", "1")
|
|
form.Set("zip_download_allowed", "1")
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users/"+target.ID, strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect, got %d", response.Code)
|
|
}
|
|
|
|
updated, ok, _ := app.store.GetUser(target.ID)
|
|
if !ok || updated.PermOverrides == nil {
|
|
t.Fatal("expected perm overrides to be set")
|
|
}
|
|
if updated.PermOverrides.UploadAllowed == nil || !*updated.PermOverrides.UploadAllowed {
|
|
t.Fatal("expected upload_allowed=true")
|
|
}
|
|
if updated.PermOverrides.ZipDownloadAllowed == nil || !*updated.PermOverrides.ZipDownloadAllowed {
|
|
t.Fatal("expected zip_download_allowed=true")
|
|
}
|
|
}
|
|
|
|
func TestUserEditLimitsUpdate(t *testing.T) {
|
|
app, admin := setupAccountTestApp(t)
|
|
target, err := app.store.CreateUserWithPassword("limits_target", "limits@example.test", "secret", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(admin.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
form.Set("username", target.Username)
|
|
form.Set("email", target.Email)
|
|
form.Set("max_file_size_bytes", "1073741824")
|
|
form.Set("max_expiry_seconds", "86400")
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users/"+target.ID, strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect, got %d", response.Code)
|
|
}
|
|
|
|
updated, ok, _ := app.store.GetUser(target.ID)
|
|
if !ok {
|
|
t.Fatal("user not found")
|
|
}
|
|
if updated.MaxFileSizeBytes == nil || *updated.MaxFileSizeBytes != 1073741824 {
|
|
t.Fatalf("expected max_file_size_bytes=1073741824, got %v", updated.MaxFileSizeBytes)
|
|
}
|
|
if updated.MaxExpirySeconds == nil || *updated.MaxExpirySeconds != 86400 {
|
|
t.Fatalf("expected max_expiry_seconds=86400, got %v", updated.MaxExpirySeconds)
|
|
}
|
|
}
|
|
|
|
func TestUserEditInvalidLimitRejected(t *testing.T) {
|
|
app, admin := setupAccountTestApp(t)
|
|
target, err := app.store.CreateUserWithPassword("badlimit", "badlimit@example.test", "secret", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(admin.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
form.Set("username", target.Username)
|
|
form.Set("email", target.Email)
|
|
form.Set("max_file_size_bytes", "notanumber")
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users/"+target.ID, strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect, got %d", response.Code)
|
|
}
|
|
location := response.Header().Get("Location")
|
|
if !strings.Contains(location, "error=") {
|
|
t.Fatalf("expected error redirect, got %q", location)
|
|
}
|
|
}
|
|
|
|
func TestUserEditSelfDisableRejected(t *testing.T) {
|
|
app, admin := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(admin.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
form.Set("username", admin.Username)
|
|
form.Set("email", admin.Email)
|
|
form.Set("state", "disabled")
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users/"+admin.ID, strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect, got %d", response.Code)
|
|
}
|
|
location := response.Header().Get("Location")
|
|
if !strings.Contains(location, "error=") {
|
|
t.Fatalf("expected error redirect, got %q", location)
|
|
}
|
|
|
|
unchanged, _, _ := app.store.GetUser(admin.ID)
|
|
if unchanged.Disabled {
|
|
t.Fatal("admin should not have been disabled")
|
|
}
|
|
}
|
|
|
|
func TestUserEditLastAdminProtected(t *testing.T) {
|
|
app, admin := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(admin.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
|
|
// try to remove admin tag from the only admin via is_admin=0 (unchecked)
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
form.Set("username", admin.Username)
|
|
form.Set("email", admin.Email)
|
|
// is_admin NOT set → wantsAdmin=false → should be blocked
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users/"+admin.ID, strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect, got %d", response.Code)
|
|
}
|
|
location := response.Header().Get("Location")
|
|
if !strings.Contains(location, "error=") {
|
|
t.Fatalf("expected error for last-admin removal, got %q", location)
|
|
}
|
|
}
|
|
|
|
func TestUserEditPasswordReset(t *testing.T) {
|
|
app, admin := setupAccountTestApp(t)
|
|
target, err := app.store.CreateUserWithPassword("resetme", "resetme@example.test", "oldpass", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(admin.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users/"+target.ID+"/password/reset", strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect, got %d", response.Code)
|
|
}
|
|
location := response.Header().Get("Location")
|
|
if !strings.Contains(location, "success=") {
|
|
t.Fatalf("expected success redirect, got %q", location)
|
|
}
|
|
|
|
updated, _, _ := app.store.GetUser(target.ID)
|
|
if metastore.VerifyPassword(updated.PasswordHash, "oldpass") {
|
|
t.Fatal("old password should no longer work after reset")
|
|
}
|
|
}
|
|
|
|
func TestUserEditRevokeSessions(t *testing.T) {
|
|
app, admin := setupAccountTestApp(t)
|
|
target, err := app.store.CreateUserWithPassword("revokeme", "revokeme@example.test", "secret", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword: %v", err)
|
|
}
|
|
targetSession, err := app.store.CreateSession(target.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
router := setupAccountTestRouter(t, app)
|
|
adminSession, err := app.store.CreateSession(admin.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", adminSession.CSRFToken)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users/"+target.ID+"/sessions/revoke", strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: adminSession.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect, got %d", response.Code)
|
|
}
|
|
|
|
_, stillValid, _ := app.store.GetSession(targetSession.Token)
|
|
if stillValid {
|
|
t.Fatal("target session should have been revoked")
|
|
}
|
|
}
|
|
|
|
func TestBulkRevokeSessions(t *testing.T) {
|
|
app, user := setupAccountTestApp(t)
|
|
router := setupAccountTestRouter(t, app)
|
|
session, err := app.store.CreateSession(user.ID, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
other, err := app.store.CreateUserWithPassword("other", "other@example.test", "secret", nil)
|
|
if err != nil {
|
|
t.Fatalf("CreateUserWithPassword returned error: %v", err)
|
|
}
|
|
if _, err := app.store.CreateSession(other.ID, time.Hour); err != nil {
|
|
t.Fatalf("CreateSession returned error: %v", err)
|
|
}
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", session.CSRFToken)
|
|
form.Set("selected_ids", other.ID)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/account/users/bulk/revoke-sessions", strings.NewReader(form.Encode()))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.AddCookie(&http.Cookie{Name: accountSessionCookie, Value: session.Token})
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect, got %d", response.Code)
|
|
}
|
|
location := response.Header().Get("Location")
|
|
if !strings.Contains(location, "Sessions revoked") {
|
|
t.Fatalf("expected success message, got %q", location)
|
|
}
|
|
}
|