2026-05-01 01:51:06 +03:00
|
|
|
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)
|
|
|
|
|
|
2026-05-01 02:14:05 +03:00
|
|
|
request := httptest.NewRequest(http.MethodPost, "/admin/settings/save", strings.NewReader(`{"values":{"api_enabled":"true","box_poll_interval_ms":"6000","global_max_file_size_gb":"0.5"}}`))
|
2026-05-01 01:51:06 +03:00
|
|
|
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)
|
|
|
|
|
}
|
2026-05-01 02:14:05 +03:00
|
|
|
if app.config.GlobalMaxFileSizeBytes != 512*1024*1024 {
|
|
|
|
|
t.Fatalf("expected size override in bytes, got %d", app.config.GlobalMaxFileSizeBytes)
|
|
|
|
|
}
|
2026-05-01 01:51:06 +03:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
2026-05-01 02:14:05 +03:00
|
|
|
if _, exists := overrides[config.SettingBoxPollIntervalMS]; !exists {
|
|
|
|
|
t.Fatalf("expected changed poll interval override to be persisted, got %#v", overrides)
|
|
|
|
|
}
|
|
|
|
|
if _, exists := overrides[config.SettingSessionTTLSeconds]; exists {
|
|
|
|
|
t.Fatalf("expected untouched setting to stay out of overrides, got %#v", overrides)
|
|
|
|
|
}
|
2026-05-01 01:51:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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())
|
|
|
|
|
}
|
2026-05-01 02:14:05 +03:00
|
|
|
if app.config.APIEnabled {
|
|
|
|
|
t.Fatal("expected reset to respect environment and restore APIEnabled=false")
|
2026-05-01 01:51:06 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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",
|
2026-05-01 02:14:05 +03:00
|
|
|
"WARPBOX_GLOBAL_MAX_FILE_SIZE_GB",
|
2026-05-01 01:51:06 +03:00
|
|
|
"WARPBOX_GLOBAL_MAX_FILE_SIZE_MB",
|
|
|
|
|
"WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES",
|
2026-05-01 02:14:05 +03:00
|
|
|
"WARPBOX_GLOBAL_MAX_BOX_SIZE_GB",
|
2026-05-01 01:51:06 +03:00
|
|
|
"WARPBOX_GLOBAL_MAX_BOX_SIZE_MB",
|
|
|
|
|
"WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES",
|
2026-05-01 02:14:05 +03:00
|
|
|
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_GB",
|
2026-05-01 01:51:06 +03:00
|
|
|
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_MB",
|
|
|
|
|
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES",
|
2026-05-01 02:14:05 +03:00
|
|
|
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_GB",
|
2026-05-01 01:51:06 +03:00
|
|
|
"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, "")
|
|
|
|
|
}
|
|
|
|
|
}
|