feat(admin): implement provider-specific storage configuration pages
Some checks failed
Build and Publish Docker Image / deploy (push) Has been cancelled

Refactor the admin storage backend creation and editing flows to use
provider-specific pages (e.g., `/admin/storage/new/sftp`) instead of a
single generic form. This ensures only relevant fields are rendered for
each storage provider (such as SFTP, S3, or WebDAV).

Additionally:
- Prevent mutation of the storage provider type during backend edits.
- Add comprehensive unit tests for provider-specific rendering, edit
  validation, and CSRF/admin route protection.
This commit is contained in:
2026-05-31 19:52:46 +03:00
parent ac9b8232f3
commit 1513030c2a
14 changed files with 2031 additions and 355 deletions

View File

@@ -1,6 +1,7 @@
package handlers
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -613,6 +614,190 @@ func TestAPIDocsHeaderReflectsLoggedOutUser(t *testing.T) {
}
}
func TestAdminStorageProviderPagesOnlyRenderRelevantFields(t *testing.T) {
app, cleanup := newTestApp(t)
defer cleanup()
adminToken := createAdminSession(t, app)
request := httptest.NewRequest(http.MethodGet, "/admin/storage/new/sftp", nil)
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
response := httptest.NewRecorder()
app.AdminNewStorageProvider(response, request)
if response.Code != http.StatusOK {
t.Fatalf("AdminNewStorageProvider status = %d, body = %s", response.Code, response.Body.String())
}
body := response.Body.String()
if !strings.Contains(body, "Private key") || !strings.Contains(body, "SSH host key") {
t.Fatalf("sftp page did not render sftp fields: %s", body)
}
for _, unwanted := range []string{"Bucket display name", "WebDAV URL", "Share</span>", "Access key"} {
if strings.Contains(body, unwanted) {
t.Fatalf("sftp page rendered irrelevant field %q: %s", unwanted, body)
}
}
cfg, err := app.uploadService.Storage().CreateBackend(services.StorageBackendConfig{
Provider: services.StorageProviderSFTP,
Name: "NAS",
Host: "files.example.test",
Username: "warpbox",
Password: "secret",
})
if err != nil {
t.Fatalf("CreateBackend returned error: %v", err)
}
editRequest := httptest.NewRequest(http.MethodGet, "/admin/storage/"+cfg.ID+"/edit", nil)
editRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
editRequest.SetPathValue("backendID", cfg.ID)
editResponse := httptest.NewRecorder()
app.AdminEditStorageForm(editResponse, editRequest)
if editResponse.Code != http.StatusOK {
t.Fatalf("AdminEditStorageForm status = %d, body = %s", editResponse.Code, editResponse.Body.String())
}
editBody := editResponse.Body.String()
if !strings.Contains(editBody, "Immutable provider") || strings.Contains(editBody, "Bucket display name") || strings.Contains(editBody, "WebDAV URL") {
t.Fatalf("edit page did not stay provider-specific: %s", editBody)
}
}
func TestAdminStorageEditRejectsProviderMutation(t *testing.T) {
app, cleanup := newTestApp(t)
defer cleanup()
adminToken := createAdminSession(t, app)
cfg, err := app.uploadService.Storage().CreateBackend(services.StorageBackendConfig{
Provider: services.StorageProviderSFTP,
Name: "NAS",
Host: "files.example.test",
Username: "warpbox",
Password: "secret",
})
if err != nil {
t.Fatalf("CreateBackend returned error: %v", err)
}
form := strings.NewReader("provider=s3&name=Changed&endpoint=https://s3.example.test&bucket=bucket&access_key=access&secret_key=secret&use_ssl=on&csrf_token=test-csrf")
request := httptest.NewRequest(http.MethodPost, "/admin/storage/"+cfg.ID+"/edit", form)
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
request.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
request.SetPathValue("backendID", cfg.ID)
response := httptest.NewRecorder()
app.AdminEditStorage(response, request)
if response.Code != http.StatusSeeOther {
t.Fatalf("AdminEditStorage status = %d, body = %s", response.Code, response.Body.String())
}
stored, err := app.uploadService.Storage().BackendConfig(cfg.ID)
if err != nil {
t.Fatalf("BackendConfig returned error: %v", err)
}
if stored.Provider != services.StorageProviderSFTP || stored.Type != services.StorageBackendSFTP || stored.Name != "NAS" {
t.Fatalf("storage backend mutated despite rejected provider change: %+v", stored)
}
}
func TestAdminStorageJobRoutesRequireAdminAndCSRF(t *testing.T) {
app, cleanup := newTestApp(t)
defer cleanup()
unauthorized := httptest.NewRecorder()
app.AdminRunStorageCleanup(unauthorized, httptest.NewRequest(http.MethodPost, "/admin/storage/jobs/cleanup", nil))
if unauthorized.Code != http.StatusSeeOther {
t.Fatalf("unauthorized cleanup status = %d", unauthorized.Code)
}
adminToken := createAdminSession(t, app)
missingCSRFRequest := httptest.NewRequest(http.MethodPost, "/admin/storage/jobs/cleanup", nil)
missingCSRFRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
missingCSRFResponse := httptest.NewRecorder()
app.AdminRunStorageCleanup(missingCSRFResponse, missingCSRFRequest)
if missingCSRFResponse.Code != http.StatusForbidden {
t.Fatalf("missing csrf cleanup status = %d", missingCSRFResponse.Code)
}
request := httptest.NewRequest(http.MethodPost, "/admin/storage/jobs/cleanup", strings.NewReader("csrf_token=test-csrf"))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
request.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
response := httptest.NewRecorder()
app.AdminRunStorageCleanup(response, request)
if response.Code != http.StatusSeeOther {
t.Fatalf("authorized cleanup status = %d, body = %s", response.Code, response.Body.String())
}
}
func TestAdminStorageSpeedTestStartsBackgroundJob(t *testing.T) {
app, cleanup := newTestApp(t)
defer cleanup()
adminToken := createAdminSession(t, app)
if _, err := app.uploadService.Storage().TestBackend(services.StorageBackendLocal); err != nil {
t.Fatalf("TestBackend returned error: %v", err)
}
request := httptest.NewRequest(http.MethodPost, "/admin/storage/local/speed-test", strings.NewReader("mode=custom&custom_file_count=2&custom_file_size_mb=0.001&csrf_token=test-csrf"))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
request.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
request.SetPathValue("backendID", services.StorageBackendLocal)
response := httptest.NewRecorder()
app.AdminStartStorageSpeedTest(response, request)
if response.Code != http.StatusSeeOther {
t.Fatalf("AdminStartStorageSpeedTest status = %d, body = %s", response.Code, response.Body.String())
}
tests, err := app.uploadService.Storage().ListSpeedTests(services.StorageBackendLocal, 10)
if err != nil {
t.Fatalf("ListSpeedTests returned error: %v", err)
}
if len(tests) != 1 {
t.Fatalf("speed tests len = %d, want 1", len(tests))
}
if tests[0].Mode != services.StorageSpeedModeCustom || tests[0].CustomFileCount != 2 || tests[0].CustomFileSizeMB != 0.001 {
t.Fatalf("custom speed test options were not stored: %+v", tests[0])
}
location := response.Header().Get("Location")
if !strings.Contains(location, "/admin/storage/local/tests") {
t.Fatalf("speed test redirect location = %q", location)
}
}
func TestAdminStorageTestingPageRendersHistory(t *testing.T) {
app, cleanup := newTestApp(t)
defer cleanup()
adminToken := createAdminSession(t, app)
if _, err := app.uploadService.Storage().TestBackend(services.StorageBackendLocal); err != nil {
t.Fatalf("TestBackend returned error: %v", err)
}
test, err := app.uploadService.Storage().StartSpeedTest(services.StorageBackendLocal, services.StorageSpeedModeSmall)
if err != nil {
t.Fatalf("StartSpeedTest returned error: %v", err)
}
app.uploadService.Storage().RunSpeedTest(context.Background(), test.ID)
request := httptest.NewRequest(http.MethodGet, "/admin/storage/local/tests", nil)
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
request.SetPathValue("backendID", services.StorageBackendLocal)
response := httptest.NewRecorder()
app.AdminStorageTests(response, request)
if response.Code != http.StatusOK {
t.Fatalf("AdminStorageTests status = %d, body = %s", response.Code, response.Body.String())
}
body := response.Body.String()
if !strings.Contains(body, "New Test") || !strings.Contains(body, "Many small files") || strings.Contains(body, "storage-test-menu") {
t.Fatalf("testing page missing expected page-based controls: %s", body)
}
jsonRequest := httptest.NewRequest(http.MethodGet, "/admin/storage/local/tests.json", nil)
jsonRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
jsonRequest.SetPathValue("backendID", services.StorageBackendLocal)
jsonResponse := httptest.NewRecorder()
app.AdminStorageTestsJSON(jsonResponse, jsonRequest)
if jsonResponse.Code != http.StatusOK {
t.Fatalf("AdminStorageTestsJSON status = %d, body = %s", jsonResponse.Code, jsonResponse.Body.String())
}
if !strings.Contains(jsonResponse.Body.String(), `"progress":100`) || !strings.Contains(jsonResponse.Body.String(), `"stage":"complete"`) {
t.Fatalf("tests json missing progress fields: %s", jsonResponse.Body.String())
}
}
func createOwnedBoxThroughApp(t *testing.T, app *App, userID string) services.UploadResult {
t.Helper()
user, err := app.authService.UserByID(userID)
@@ -638,6 +823,19 @@ func createOwnedBoxThroughApp(t *testing.T, app *App, userID string) services.Up
return payload
}
func createAdminSession(t *testing.T, app *App) string {
t.Helper()
_, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
if err != nil && !strings.Contains(err.Error(), "registration is closed") {
t.Fatalf("CreateBootstrapUser returned error: %v", err)
}
_, token, err := app.authService.Login("admin@example.test", "password123")
if err != nil {
t.Fatalf("Login returned error: %v", err)
}
return token
}
func testPolicy(t *testing.T, app *App) services.UploadPolicySettings {
t.Helper()
policy, err := app.settingsService.UploadPolicy()