package server import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/gin-gonic/gin" "warpbox/lib/config" ) func TestAdminSettingsRequiresAuth(t *testing.T) { app, router := setupAdminSettingsTest(t) request := httptest.NewRequest(http.MethodGet, "/admin/settings", nil) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusSeeOther { t.Fatalf("expected redirect, got %d", response.Code) } if location := response.Header().Get("Location"); location != "/admin/login" { t.Fatalf("expected login redirect, got %q", location) } if app == nil { t.Fatal("expected app setup") } } func TestAdminSettingsPageRenders(t *testing.T) { app, router := setupAdminSettingsTest(t) request := httptest.NewRequest(http.MethodGet, "/admin/settings", nil) request.AddCookie(authCookie(app)) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("expected 200, got %d", response.Code) } body := response.Body.String() if !strings.Contains(body, "WarpBox Settings") { t.Fatalf("expected settings page title, got %s", body) } if !strings.Contains(body, "WARPBOX_API_ENABLED") { t.Fatalf("expected API env var in page body") } } func TestAdminSettingsExportIncludesCurrentValues(t *testing.T) { app, router := setupAdminSettingsTest(t) request := httptest.NewRequest(http.MethodGet, "/admin/settings/export", nil) request.AddCookie(authCookie(app)) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("expected 200, got %d", response.Code) } var payload struct { Format string `json:"format"` Settings map[string]string `json:"settings"` } if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil { t.Fatalf("json.Unmarshal returned error: %v", err) } if payload.Format != "warpbox.settings.export.v1" { t.Fatalf("unexpected export format: %q", payload.Format) } if payload.Settings[config.SettingAPIEnabled] != "false" { t.Fatalf("expected api_enabled to reflect environment false, got %q", payload.Settings[config.SettingAPIEnabled]) } } func TestAdminSettingsSavePersistsEditableOverrides(t *testing.T) { app, router := setupAdminSettingsTest(t) request := httptest.NewRequest(http.MethodPost, "/admin/settings/save", strings.NewReader(`{"values":{"api_enabled":"true","box_poll_interval_ms":"6000"}}`)) request.Header.Set("Content-Type", "application/json") request.AddCookie(authCookie(app)) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", response.Code, response.Body.String()) } if !app.config.APIEnabled { t.Fatal("expected APIEnabled override to be applied") } if app.config.BoxPollIntervalMS != 6000 { t.Fatalf("expected poll interval override, got %d", app.config.BoxPollIntervalMS) } overrides, err := config.ReadAdminSettingsOverrides(app.settingsOverridesPath) if err != nil { t.Fatalf("ReadAdminSettingsOverrides returned error: %v", err) } if overrides[config.SettingAPIEnabled] != "true" { t.Fatalf("expected persisted API override, got %#v", overrides) } } func TestAdminSettingsSaveRejectsLockedSetting(t *testing.T) { app, router := setupAdminSettingsTest(t) request := httptest.NewRequest(http.MethodPost, "/admin/settings/save", strings.NewReader(`{"values":{"data_dir":"./other"}}`)) request.Header.Set("Content-Type", "application/json") request.AddCookie(authCookie(app)) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", response.Code) } } func TestAdminSettingsImportSkipsLockedAndUnknownKeys(t *testing.T) { app, router := setupAdminSettingsTest(t) request := httptest.NewRequest(http.MethodPost, "/admin/settings/import", strings.NewReader(`{"settings":{"api_enabled":"true","data_dir":"./other","bogus":"x"}}`)) request.Header.Set("Content-Type", "application/json") request.AddCookie(authCookie(app)) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", response.Code, response.Body.String()) } if !app.config.APIEnabled { t.Fatal("expected editable import value to apply") } var payload struct { Warnings []string `json:"warnings"` } if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil { t.Fatalf("json.Unmarshal returned error: %v", err) } if len(payload.Warnings) != 2 { t.Fatalf("expected 2 warnings, got %#v", payload.Warnings) } } func TestAdminSettingsResetUsesBuiltInDefaults(t *testing.T) { app, router := setupAdminSettingsTest(t) request := httptest.NewRequest(http.MethodPost, "/admin/settings/reset", strings.NewReader(`{}`)) request.Header.Set("Content-Type", "application/json") request.AddCookie(authCookie(app)) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("expected 200, got %d: %s", response.Code, response.Body.String()) } if !app.config.APIEnabled { t.Fatal("expected reset to built-in defaults to restore APIEnabled=true") } } func setupAdminSettingsTest(t *testing.T) (*App, *gin.Engine) { t.Helper() gin.SetMode(gin.TestMode) cwd, err := os.Getwd() if err != nil { t.Fatalf("Getwd returned error: %v", err) } root := filepath.Clean(filepath.Join(cwd, "..", "..")) if err := os.Chdir(root); err != nil { t.Fatalf("Chdir returned error: %v", err) } t.Cleanup(func() { _ = os.Chdir(cwd) }) clearAdminSettingsEnv(t) t.Setenv("WARPBOX_DATA_DIR", t.TempDir()) t.Setenv("WARPBOX_ADMIN_PASSWORD", "secret") t.Setenv("WARPBOX_ADMIN_ENABLED", "true") t.Setenv("WARPBOX_API_ENABLED", "false") cfg, err := config.Load() if err != nil { t.Fatalf("Load returned error: %v", err) } if err := cfg.EnsureDirectories(); err != nil { t.Fatalf("EnsureDirectories returned error: %v", err) } app := &App{ config: cfg, settingsOverridesPath: filepath.Join(cfg.DBDir, config.AdminSettingsOverrideFilename), } htmlTemplates, err := loadHTMLTemplates() if err != nil { t.Fatalf("loadHTMLTemplates returned error: %v", err) } router := gin.New() router.SetHTMLTemplate(htmlTemplates) admin := router.Group("/admin") admin.GET("/login", app.handleAdminLogin) protected := router.Group("/admin", app.adminAuthMiddleware) protected.GET("/settings", app.handleAdminSettings) protected.GET("/settings/export", app.handleAdminSettingsExport) protected.POST("/settings/save", app.handleAdminSettingsSave) protected.POST("/settings/import", app.handleAdminSettingsImport) protected.POST("/settings/reset", app.handleAdminSettingsReset) return app, router } func authCookie(app *App) *http.Cookie { return &http.Cookie{Name: adminSessionCookie, Value: app.adminSessionToken()} } func clearAdminSettingsEnv(t *testing.T) { t.Helper() for _, name := range []string{ "WARPBOX_DATA_DIR", "WARPBOX_ADMIN_PASSWORD", "WARPBOX_ADMIN_USERNAME", "WARPBOX_ADMIN_EMAIL", "WARPBOX_ADMIN_ENABLED", "WARPBOX_ALLOW_ADMIN_SETTINGS_OVERRIDE", "WARPBOX_ADMIN_COOKIE_SECURE", "WARPBOX_GUEST_UPLOADS_ENABLED", "WARPBOX_API_ENABLED", "WARPBOX_ZIP_DOWNLOADS_ENABLED", "WARPBOX_ONE_TIME_DOWNLOADS_ENABLED", "WARPBOX_ONE_TIME_DOWNLOAD_EXPIRY_SECONDS", "WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE", "WARPBOX_RENEW_ON_ACCESS_ENABLED", "WARPBOX_RENEW_ON_DOWNLOAD_ENABLED", "WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS", "WARPBOX_MAX_GUEST_EXPIRY_SECONDS", "WARPBOX_GLOBAL_MAX_FILE_SIZE_MB", "WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES", "WARPBOX_GLOBAL_MAX_BOX_SIZE_MB", "WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES", "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_MB", "WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES", "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_MB", "WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES", "WARPBOX_SESSION_TTL_SECONDS", "WARPBOX_BOX_POLL_INTERVAL_MS", "WARPBOX_THUMBNAIL_BATCH_SIZE", "WARPBOX_THUMBNAIL_INTERVAL_SECONDS", } { t.Setenv(name, "") } }