Remove hyphens from compound adjectives such as "logged-in", "one-time", "password-protected", "full-height", "multi-file", and "S3-compatible" in comments, test error messages, and UI labels to improve readability and consistency.
1319 lines
54 KiB
Go
1319 lines
54 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"warpbox.dev/backend/libs/services"
|
|
)
|
|
|
|
func TestLoggedInUploadStoresOwnerAndAnonymousUploadDoesNot(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
user, err := app.authService.CreateBootstrapUser("daniel", "daniel@example.test", "password123")
|
|
if err != nil {
|
|
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
|
}
|
|
_, token, err := app.authService.Login("daniel@example.test", "password123")
|
|
if err != nil {
|
|
t.Fatalf("Login returned error: %v", err)
|
|
}
|
|
|
|
request := multipartUploadRequest(t, "/api/v1/upload", "file", "owned.txt", "owned")
|
|
request.Header.Set("Accept", "application/json")
|
|
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
|
response := httptest.NewRecorder()
|
|
app.Upload(response, request)
|
|
if response.Code != http.StatusCreated {
|
|
t.Fatalf("owned upload status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
var ownedPayload services.UploadResult
|
|
if err := json.Unmarshal(response.Body.Bytes(), &ownedPayload); err != nil {
|
|
t.Fatalf("json.Unmarshal owned returned error: %v", err)
|
|
}
|
|
ownedBox, err := app.uploadService.GetBox(ownedPayload.BoxID)
|
|
if err != nil {
|
|
t.Fatalf("GetBox owned returned error: %v", err)
|
|
}
|
|
if ownedBox.OwnerID != user.ID {
|
|
t.Fatalf("owned OwnerID = %q, want %q", ownedBox.OwnerID, user.ID)
|
|
}
|
|
|
|
owned := uploadThroughApp(t, app)
|
|
anonymous, err := app.uploadService.GetBox(owned.BoxID)
|
|
if err != nil {
|
|
t.Fatalf("GetBox anonymous returned error: %v", err)
|
|
}
|
|
if anonymous.OwnerID != "" {
|
|
t.Fatalf("anonymous OwnerID = %q, want empty", anonymous.OwnerID)
|
|
}
|
|
|
|
boxes, err := app.uploadService.ListBoxes(0)
|
|
if err != nil {
|
|
t.Fatalf("ListBoxes returned error: %v", err)
|
|
}
|
|
foundOwned := false
|
|
for _, box := range boxes {
|
|
if box.OwnerID == user.ID {
|
|
foundOwned = true
|
|
}
|
|
}
|
|
if !foundOwned {
|
|
t.Fatalf("logged in upload did not store owner id %q", user.ID)
|
|
}
|
|
}
|
|
|
|
func TestBearerTokenUploadActsAsUser(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
user, err := app.authService.CreateBootstrapUser("daniel", "daniel@example.test", "password123")
|
|
if err != nil {
|
|
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
|
}
|
|
tokenResult, err := app.authService.CreateAPIToken(user.ID, "cli")
|
|
if err != nil {
|
|
t.Fatalf("CreateAPIToken returned error: %v", err)
|
|
}
|
|
|
|
request := multipartUploadRequest(t, "/api/v1/upload", "file", "owned.txt", "owned")
|
|
request.Header.Set("Accept", "application/json")
|
|
request.Header.Set("Authorization", "Bearer "+tokenResult.Plaintext)
|
|
response := httptest.NewRecorder()
|
|
app.Upload(response, request)
|
|
if response.Code != http.StatusCreated {
|
|
t.Fatalf("token upload status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
var payload services.UploadResult
|
|
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("json.Unmarshal returned error: %v", err)
|
|
}
|
|
box, err := app.uploadService.GetBox(payload.BoxID)
|
|
if err != nil {
|
|
t.Fatalf("GetBox returned error: %v", err)
|
|
}
|
|
if box.OwnerID != user.ID {
|
|
t.Fatalf("OwnerID = %q, want %q", box.OwnerID, user.ID)
|
|
}
|
|
|
|
// An invalid bearer token is an authentication failure, not an anonymous upload.
|
|
badRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "x.txt", "x")
|
|
badRequest.Header.Set("Accept", "application/json")
|
|
badRequest.Header.Set("Authorization", "Bearer wbx_bogus.secret")
|
|
badResponse := httptest.NewRecorder()
|
|
app.Upload(badResponse, badRequest)
|
|
if badResponse.Code != http.StatusUnauthorized {
|
|
t.Fatalf("invalid token upload status = %d, body = %s", badResponse.Code, badResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAnonymousUploadWithoutBearerStillWorks(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
response := httptest.NewRecorder()
|
|
app.Upload(response, multipartUploadRequest(t, "/api/v1/upload", "file", "anonymous.txt", "anonymous"))
|
|
if response.Code != http.StatusCreated {
|
|
t.Fatalf("anonymous upload status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDisabledUserBearerTokenCannotUpload(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
user, err := app.authService.CreateBootstrapUser("daniel", "daniel@example.test", "password123")
|
|
if err != nil {
|
|
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
|
}
|
|
tokenResult, err := app.authService.CreateAPIToken(user.ID, "cli")
|
|
if err != nil {
|
|
t.Fatalf("CreateAPIToken returned error: %v", err)
|
|
}
|
|
if err := app.authService.DisableUser(user.ID, true); err != nil {
|
|
t.Fatalf("DisableUser returned error: %v", err)
|
|
}
|
|
|
|
request := multipartUploadRequest(t, "/api/v1/upload", "file", "blocked.txt", "blocked")
|
|
request.Header.Set("Accept", "application/json")
|
|
request.Header.Set("Authorization", "Bearer "+tokenResult.Plaintext)
|
|
response := httptest.NewRecorder()
|
|
app.Upload(response, request)
|
|
if response.Code != http.StatusUnauthorized {
|
|
t.Fatalf("disabled bearer upload status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestInviteHandlerCreatesUserAndMarksInviteUsed(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
admin, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
|
if err != nil {
|
|
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
|
}
|
|
invite, err := app.authService.CreateInvite("friend@example.test", services.UserRoleUser, admin.ID, 0)
|
|
if err != nil {
|
|
t.Fatalf("CreateInvite returned error: %v", err)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/invite/"+invite.Token, strings.NewReader("username=friend&password=password123"))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.SetPathValue("token", invite.Token)
|
|
response := httptest.NewRecorder()
|
|
app.InvitePost(response, request)
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("InvitePost status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
if _, err := app.authService.AcceptInvite(invite.Token, "friend", "password123"); err == nil {
|
|
t.Fatalf("invite token remained reusable")
|
|
}
|
|
}
|
|
|
|
func TestNonOwnerCannotManageOwnedBox(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
owner, err := app.authService.CreateBootstrapUser("owner", "owner@example.test", "password123")
|
|
if err != nil {
|
|
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
|
}
|
|
invite, err := app.authService.CreateInvite("other@example.test", services.UserRoleUser, owner.ID, 0)
|
|
if err != nil {
|
|
t.Fatalf("CreateInvite returned error: %v", err)
|
|
}
|
|
other, err := app.authService.AcceptInvite(invite.Token, "other", "password123")
|
|
if err != nil {
|
|
t.Fatalf("AcceptInvite returned error: %v", err)
|
|
}
|
|
|
|
result := createOwnedBoxThroughApp(t, app, owner.ID)
|
|
if err := app.uploadService.RenameOwnedBox(result.BoxID, other.ID, "stolen"); err == nil {
|
|
t.Fatalf("RenameOwnedBox allowed non-owner")
|
|
}
|
|
}
|
|
|
|
func TestAdminUploadBypassesMaxUploadSize(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
_, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
|
if err != nil {
|
|
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)
|
|
}
|
|
|
|
request := multipartUploadRequest(t, "/api/v1/upload", "file", "large.txt", strings.Repeat("x", int(app.uploadService.MaxUploadSize())+1))
|
|
request.Header.Set("Accept", "application/json")
|
|
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
|
response := httptest.NewRecorder()
|
|
app.Upload(response, request)
|
|
if response.Code != http.StatusCreated {
|
|
t.Fatalf("admin upload status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestUnlimitedAnonymousUploadPolicyUsesNegativeOne(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
policy, err := app.settingsService.UploadPolicy()
|
|
if err != nil {
|
|
t.Fatalf("UploadPolicy returned error: %v", err)
|
|
}
|
|
policy.AnonymousMaxUploadMB = -1
|
|
policy.AnonymousDailyUploadMB = -1
|
|
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
|
|
request := multipartUploadRequest(t, "/api/v1/upload", "file", "large.txt", strings.Repeat("x", int(app.uploadService.MaxUploadSize())+1))
|
|
request.Header.Set("Accept", "application/json")
|
|
response := httptest.NewRecorder()
|
|
app.Upload(response, request)
|
|
if response.Code != http.StatusCreated {
|
|
t.Fatalf("unlimited anonymous upload status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAnonymousUploadDisabled(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
policy := testPolicy(t, app)
|
|
policy.AnonymousUploadsEnabled = false
|
|
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
|
|
request := multipartUploadRequest(t, "/api/v1/upload", "file", "note.txt", "hello")
|
|
request.Header.Set("Accept", "application/json")
|
|
response := httptest.NewRecorder()
|
|
app.Upload(response, request)
|
|
if response.Code != http.StatusForbidden {
|
|
t.Fatalf("status = %d, want 403, body = %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAnonymousUploadLimits(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
policy := testPolicy(t, app)
|
|
policy.AnonymousMaxUploadMB = 1
|
|
policy.AnonymousDailyUploadMB = 0.001
|
|
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
|
|
large := multipartUploadRequest(t, "/api/v1/upload", "file", "large.txt", strings.Repeat("x", 2*1024*1024))
|
|
large.Header.Set("Accept", "application/json")
|
|
large.RemoteAddr = "192.0.2.10:1234"
|
|
largeResponse := httptest.NewRecorder()
|
|
app.Upload(largeResponse, large)
|
|
if largeResponse.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("large status = %d, body = %s", largeResponse.Code, largeResponse.Body.String())
|
|
}
|
|
|
|
daily := multipartUploadRequest(t, "/api/v1/upload", "file", "note.txt", strings.Repeat("x", 2048))
|
|
daily.Header.Set("Accept", "application/json")
|
|
daily.RemoteAddr = "192.0.2.10:1234"
|
|
dailyResponse := httptest.NewRecorder()
|
|
app.Upload(dailyResponse, daily)
|
|
if dailyResponse.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("daily status = %d, body = %s", dailyResponse.Code, dailyResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSignedInUploadQuotaAndOverride(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
user, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
|
if err != nil {
|
|
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
|
}
|
|
invite, err := app.authService.CreateInvite("user@example.test", services.UserRoleUser, user.ID, 0)
|
|
if err != nil {
|
|
t.Fatalf("CreateInvite returned error: %v", err)
|
|
}
|
|
normal, err := app.authService.AcceptInvite(invite.Token, "user", "password123")
|
|
if err != nil {
|
|
t.Fatalf("AcceptInvite returned error: %v", err)
|
|
}
|
|
_, token, err := app.authService.Login(normal.Email, "password123")
|
|
if err != nil {
|
|
t.Fatalf("Login returned error: %v", err)
|
|
}
|
|
policy := testPolicy(t, app)
|
|
policy.DefaultUserStorageMB = 0.001
|
|
policy.UserDailyUploadMB = 8
|
|
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
|
|
request := multipartUploadRequest(t, "/api/v1/upload", "file", "quota.txt", strings.Repeat("x", 2048))
|
|
request.Header.Set("Accept", "application/json")
|
|
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
|
response := httptest.NewRecorder()
|
|
app.Upload(response, request)
|
|
if response.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("quota status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
|
|
override := 10.0
|
|
if err := app.authService.SetUserStorageQuota(normal.ID, &override); err != nil {
|
|
t.Fatalf("SetUserStorageQuota returned error: %v", err)
|
|
}
|
|
request = multipartUploadRequest(t, "/api/v1/upload", "file", "quota.txt", strings.Repeat("x", 2048))
|
|
request.Header.Set("Accept", "application/json")
|
|
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
|
response = httptest.NewRecorder()
|
|
app.Upload(response, request)
|
|
if response.Code != http.StatusCreated {
|
|
t.Fatalf("override status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSignedInDailyCap(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
admin, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
|
if err != nil {
|
|
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
|
}
|
|
invite, err := app.authService.CreateInvite("user@example.test", services.UserRoleUser, admin.ID, 0)
|
|
if err != nil {
|
|
t.Fatalf("CreateInvite returned error: %v", err)
|
|
}
|
|
user, err := app.authService.AcceptInvite(invite.Token, "user", "password123")
|
|
if err != nil {
|
|
t.Fatalf("AcceptInvite returned error: %v", err)
|
|
}
|
|
_, token, err := app.authService.Login(user.Email, "password123")
|
|
if err != nil {
|
|
t.Fatalf("Login returned error: %v", err)
|
|
}
|
|
policy := testPolicy(t, app)
|
|
policy.UserDailyUploadMB = 0.001
|
|
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
request := multipartUploadRequest(t, "/api/v1/upload", "file", "daily.txt", strings.Repeat("x", 2048))
|
|
request.Header.Set("Accept", "application/json")
|
|
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
|
response := httptest.NewRecorder()
|
|
app.Upload(response, request)
|
|
if response.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("daily status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestLayeredUploadLimits(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
policy := testPolicy(t, app)
|
|
policy.AnonymousDailyBoxes = 1
|
|
policy.AnonymousActiveBoxes = 10
|
|
policy.AnonymousMaxDays = 3
|
|
policy.LocalStorageMaxGB = 0.001
|
|
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
|
|
first := uploadThroughApp(t, app)
|
|
if first.BoxID == "" {
|
|
t.Fatalf("first upload did not return a box id")
|
|
}
|
|
secondRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "second.txt", "hello")
|
|
secondRequest.Header.Set("Accept", "application/json")
|
|
secondResponse := httptest.NewRecorder()
|
|
app.Upload(secondResponse, secondRequest)
|
|
if secondResponse.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("daily box status = %d, body = %s", secondResponse.Code, secondResponse.Body.String())
|
|
}
|
|
|
|
policy.AnonymousDailyBoxes = 10
|
|
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
expiryRequest := multipartUploadRequestWithField(t, "/api/v1/upload", "file", "expiry.txt", "hello", "max_days", "30")
|
|
expiryRequest.Header.Set("Accept", "application/json")
|
|
expiryResponse := httptest.NewRecorder()
|
|
app.Upload(expiryResponse, expiryRequest)
|
|
if expiryResponse.Code != http.StatusRequestEntityTooLarge && expiryResponse.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("expiry/box status = %d, body = %s", expiryResponse.Code, expiryResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestBatchedUploadAppendBypassesDailyBoxCreationCap(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
policy := testPolicy(t, app)
|
|
policy.AnonymousDailyBoxes = 1
|
|
policy.AnonymousActiveBoxes = 10
|
|
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
|
|
first := multipartUploadRequest(t, "/api/v1/upload", "file", "first.txt", "hello")
|
|
first.Header.Set("Accept", "application/json")
|
|
first.Header.Set(uploadBatchHeader, "sharex-test")
|
|
firstResponse := httptest.NewRecorder()
|
|
app.Upload(firstResponse, first)
|
|
if firstResponse.Code != http.StatusCreated {
|
|
t.Fatalf("first batched status = %d, body = %s", firstResponse.Code, firstResponse.Body.String())
|
|
}
|
|
|
|
second := multipartUploadRequest(t, "/api/v1/upload", "file", "second.txt", "hello")
|
|
second.Header.Set("Accept", "application/json")
|
|
second.Header.Set(uploadBatchHeader, "sharex-test")
|
|
secondResponse := httptest.NewRecorder()
|
|
app.Upload(secondResponse, second)
|
|
if secondResponse.Code != http.StatusCreated {
|
|
t.Fatalf("second batched status = %d, body = %s", secondResponse.Code, secondResponse.Body.String())
|
|
}
|
|
|
|
third := multipartUploadRequest(t, "/api/v1/upload", "file", "third.txt", "hello")
|
|
third.Header.Set("Accept", "application/json")
|
|
thirdResponse := httptest.NewRecorder()
|
|
app.Upload(thirdResponse, third)
|
|
if thirdResponse.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("non-batched status = %d, body = %s", thirdResponse.Code, thirdResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestBatchedUploadAppendBypassesActiveBoxCreationCap(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
policy := testPolicy(t, app)
|
|
policy.AnonymousDailyBoxes = 10
|
|
policy.AnonymousActiveBoxes = 1
|
|
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
|
|
first := multipartUploadRequest(t, "/api/v1/upload", "file", "first.txt", "hello")
|
|
first.Header.Set("Accept", "application/json")
|
|
first.Header.Set(uploadBatchHeader, "active-cap")
|
|
firstResponse := httptest.NewRecorder()
|
|
app.Upload(firstResponse, first)
|
|
if firstResponse.Code != http.StatusCreated {
|
|
t.Fatalf("first batched status = %d, body = %s", firstResponse.Code, firstResponse.Body.String())
|
|
}
|
|
|
|
second := multipartUploadRequest(t, "/api/v1/upload", "file", "second.txt", "hello")
|
|
second.Header.Set("Accept", "application/json")
|
|
second.Header.Set(uploadBatchHeader, "active-cap")
|
|
secondResponse := httptest.NewRecorder()
|
|
app.Upload(secondResponse, second)
|
|
if secondResponse.Code != http.StatusCreated {
|
|
t.Fatalf("second batched status = %d, body = %s", secondResponse.Code, secondResponse.Body.String())
|
|
}
|
|
|
|
third := multipartUploadRequest(t, "/api/v1/upload", "file", "third.txt", "hello")
|
|
third.Header.Set("Accept", "application/json")
|
|
thirdResponse := httptest.NewRecorder()
|
|
app.Upload(thirdResponse, third)
|
|
if thirdResponse.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("non-batched status = %d, body = %s", thirdResponse.Code, thirdResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestUserPolicyOverrideChangesUploadEnforcement(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
admin, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
|
if err != nil {
|
|
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
|
}
|
|
invite, err := app.authService.CreateInvite("user@example.test", services.UserRoleUser, admin.ID, 0)
|
|
if err != nil {
|
|
t.Fatalf("CreateInvite returned error: %v", err)
|
|
}
|
|
user, err := app.authService.AcceptInvite(invite.Token, "user", "password123")
|
|
if err != nil {
|
|
t.Fatalf("AcceptInvite returned error: %v", err)
|
|
}
|
|
dailyBoxes := 1
|
|
maxDays := 1
|
|
if err := app.authService.SetUserPolicy(user.ID, services.UserPolicy{DailyBoxes: &dailyBoxes, MaxDays: &maxDays}); err != nil {
|
|
t.Fatalf("SetUserPolicy returned error: %v", err)
|
|
}
|
|
_, token, err := app.authService.Login(user.Email, "password123")
|
|
if err != nil {
|
|
t.Fatalf("Login returned error: %v", err)
|
|
}
|
|
first := multipartUploadRequest(t, "/api/v1/upload", "file", "one.txt", "hello")
|
|
first.Header.Set("Accept", "application/json")
|
|
first.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
|
firstResponse := httptest.NewRecorder()
|
|
app.Upload(firstResponse, first)
|
|
if firstResponse.Code != http.StatusCreated {
|
|
t.Fatalf("first status = %d, body = %s", firstResponse.Code, firstResponse.Body.String())
|
|
}
|
|
second := multipartUploadRequest(t, "/api/v1/upload", "file", "two.txt", "hello")
|
|
second.Header.Set("Accept", "application/json")
|
|
second.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
|
secondResponse := httptest.NewRecorder()
|
|
app.Upload(secondResponse, second)
|
|
if secondResponse.Code != http.StatusTooManyRequests {
|
|
t.Fatalf("second status = %d, body = %s", secondResponse.Code, secondResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestLocalStorageCapRejectsUpload(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
policy := testPolicy(t, app)
|
|
policy.AnonymousMaxUploadMB = 4
|
|
policy.AnonymousDailyUploadMB = 8
|
|
policy.LocalStorageMaxGB = 0.001
|
|
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
request := multipartUploadRequest(t, "/api/v1/upload", "file", "large.txt", strings.Repeat("x", 2*1024*1024))
|
|
request.Header.Set("Accept", "application/json")
|
|
response := httptest.NewRecorder()
|
|
app.Upload(response, request)
|
|
if response.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("storage cap status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAdminSettingsPostChangesUploadEnforcement(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
_, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
|
if err != nil {
|
|
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)
|
|
}
|
|
|
|
settingsForm := strings.NewReader("anonymous_max_upload_mb=512&anonymous_daily_upload_mb=2048&user_daily_upload_mb=8192&default_user_storage_mb=51200&usage_retention_days=30&csrf_token=test-csrf")
|
|
settingsRequest := httptest.NewRequest(http.MethodPost, "/admin/settings", settingsForm)
|
|
settingsRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
settingsRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
|
settingsRequest.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
|
|
settingsResponse := httptest.NewRecorder()
|
|
app.AdminSettingsPost(settingsResponse, settingsRequest)
|
|
if settingsResponse.Code != http.StatusSeeOther {
|
|
t.Fatalf("AdminSettingsPost status = %d, body = %s", settingsResponse.Code, settingsResponse.Body.String())
|
|
}
|
|
|
|
uploadRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "note.txt", "hello")
|
|
uploadRequest.Header.Set("Accept", "application/json")
|
|
uploadResponse := httptest.NewRecorder()
|
|
app.Upload(uploadResponse, uploadRequest)
|
|
if uploadResponse.Code != http.StatusForbidden {
|
|
t.Fatalf("upload status = %d, want 403, body = %s", uploadResponse.Code, uploadResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAdminUserQuotaPostChangesEnforcement(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
admin, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
|
if err != nil {
|
|
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
|
}
|
|
invite, err := app.authService.CreateInvite("user@example.test", services.UserRoleUser, admin.ID, 0)
|
|
if err != nil {
|
|
t.Fatalf("CreateInvite returned error: %v", err)
|
|
}
|
|
user, err := app.authService.AcceptInvite(invite.Token, "user", "password123")
|
|
if err != nil {
|
|
t.Fatalf("AcceptInvite returned error: %v", err)
|
|
}
|
|
_, adminToken, err := app.authService.Login(admin.Email, "password123")
|
|
if err != nil {
|
|
t.Fatalf("admin Login returned error: %v", err)
|
|
}
|
|
|
|
quotaRequest := httptest.NewRequest(http.MethodPost, "/admin/users/"+user.ID+"/quota", strings.NewReader("storage_quota_mb=0.001&csrf_token=test-csrf"))
|
|
quotaRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
quotaRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
|
quotaRequest.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
|
|
quotaRequest.SetPathValue("userID", user.ID)
|
|
quotaResponse := httptest.NewRecorder()
|
|
app.AdminUpdateUserQuota(quotaResponse, quotaRequest)
|
|
if quotaResponse.Code != http.StatusSeeOther {
|
|
t.Fatalf("AdminUpdateUserQuota status = %d, body = %s", quotaResponse.Code, quotaResponse.Body.String())
|
|
}
|
|
|
|
_, userToken, err := app.authService.Login(user.Email, "password123")
|
|
if err != nil {
|
|
t.Fatalf("user Login returned error: %v", err)
|
|
}
|
|
uploadRequest := multipartUploadRequest(t, "/api/v1/upload", "file", "quota.txt", strings.Repeat("x", 2048))
|
|
uploadRequest.Header.Set("Accept", "application/json")
|
|
uploadRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: userToken})
|
|
uploadResponse := httptest.NewRecorder()
|
|
app.Upload(uploadResponse, uploadRequest)
|
|
if uploadResponse.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("upload status = %d, want 413, body = %s", uploadResponse.Code, uploadResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestHomeReflectsUploadPolicySettings(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
policy := testPolicy(t, app)
|
|
policy.AnonymousMaxUploadMB = 123
|
|
policy.AnonymousDailyUploadMB = 456
|
|
if err := app.settingsService.UpdateUploadPolicy(policy); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
response := httptest.NewRecorder()
|
|
app.Home(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("Home status = %d", response.Code)
|
|
}
|
|
body := response.Body.String()
|
|
if !strings.Contains(body, "Max file size: 123 MB") || !strings.Contains(body, "456 MB") {
|
|
t.Fatalf("home did not reflect policy settings: %s", body)
|
|
}
|
|
if !strings.Contains(body, "warpbox.dev · test ·") {
|
|
t.Fatalf("home footer did not include app version: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestAPIDocsHeaderReflectsLoggedInUser(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
_, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
|
if err != nil {
|
|
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)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/api", nil)
|
|
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
|
response := httptest.NewRecorder()
|
|
app.APIDocs(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("APIDocs status = %d", response.Code)
|
|
}
|
|
body := response.Body.String()
|
|
header := body[:strings.Index(body, "<main")]
|
|
if !strings.Contains(header, "Dashboard") || strings.Contains(header, "Sign in") || strings.Contains(header, "Health") {
|
|
t.Fatalf("api header did not reflect logged in state: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestAPIDocsHeaderReflectsLoggedOutUser(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/api", nil)
|
|
response := httptest.NewRecorder()
|
|
app.APIDocs(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("APIDocs status = %d", response.Code)
|
|
}
|
|
body := response.Body.String()
|
|
header := body[:strings.Index(body, "<main")]
|
|
if !strings.Contains(header, "Sign in") || !strings.Contains(header, ">API<") || strings.Contains(header, "Health") || strings.Contains(header, "Dashboard") {
|
|
t.Fatalf("api header did not reflect logged-out state: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestAdminOverviewChartsUseZeroAndFullHeights(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
today := time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, time.UTC)
|
|
overview := buildAdminOverview([]services.AdminBox{{
|
|
ID: "box1",
|
|
CreatedAt: today,
|
|
TotalSize: 1024,
|
|
}}, services.AdminStats{TotalBoxes: 1, TotalFiles: 1, TotalSize: 1024})
|
|
|
|
for i, bar := range overview.UploadDays {
|
|
want := 0
|
|
if i == len(overview.UploadDays)-1 {
|
|
want = 150
|
|
}
|
|
if bar.HeightPx != want {
|
|
t.Fatalf("upload bar %d height = %d, want %d", i, bar.HeightPx, want)
|
|
}
|
|
}
|
|
for i, bar := range overview.StorageDays {
|
|
want := 0
|
|
if i == len(overview.StorageDays)-1 {
|
|
want = 150
|
|
}
|
|
if bar.HeightPx != want {
|
|
t.Fatalf("storage bar %d height = %d, want %d", i, bar.HeightPx, want)
|
|
}
|
|
}
|
|
if overview.StatusBars[0].WidthPercent != 100 {
|
|
t.Fatalf("active status width = %d, want 100", overview.StatusBars[0].WidthPercent)
|
|
}
|
|
}
|
|
|
|
func TestAdminOverviewChartsScaleRelativeToVisibleRange(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
today := time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, time.UTC)
|
|
yesterday := today.AddDate(0, 0, -1)
|
|
twoDaysAgo := today.AddDate(0, 0, -2)
|
|
boxes := []services.AdminBox{
|
|
{ID: "today-1", CreatedAt: today, TotalSize: 30},
|
|
{ID: "today-2", CreatedAt: today, TotalSize: 30},
|
|
{ID: "today-3", CreatedAt: today, TotalSize: 30},
|
|
{ID: "yesterday-1", CreatedAt: yesterday, TotalSize: 20},
|
|
{ID: "yesterday-2", CreatedAt: yesterday, TotalSize: 20},
|
|
{ID: "two-days-ago", CreatedAt: twoDaysAgo, TotalSize: 10},
|
|
}
|
|
overview := buildAdminOverview(boxes, services.AdminStats{TotalBoxes: 6, ExpiredBoxes: 2, ProtectedBoxes: 1})
|
|
|
|
last := len(overview.UploadDays) - 1
|
|
if overview.UploadDays[last].HeightPx != 150 {
|
|
t.Fatalf("3-upload day height = %d, want 150", overview.UploadDays[last].HeightPx)
|
|
}
|
|
if overview.UploadDays[last-1].HeightPx != 100 {
|
|
t.Fatalf("2-upload day height = %d, want 100", overview.UploadDays[last-1].HeightPx)
|
|
}
|
|
if overview.UploadDays[last-2].HeightPx != 50 {
|
|
t.Fatalf("1-upload day height = %d, want 50", overview.UploadDays[last-2].HeightPx)
|
|
}
|
|
if overview.StorageDays[last].HeightPx != 150 || overview.StorageDays[last-1].HeightPx != 66 || overview.StorageDays[last-2].HeightPx != 16 {
|
|
t.Fatalf("storage heights = %d/%d/%d, want 150/66/16", overview.StorageDays[last].HeightPx, overview.StorageDays[last-1].HeightPx, overview.StorageDays[last-2].HeightPx)
|
|
}
|
|
if overview.StatusBars[0].WidthPercent != 100 || overview.StatusBars[1].WidthPercent != 50 || overview.StatusBars[2].WidthPercent != 25 {
|
|
t.Fatalf("status widths = %d/%d/%d, want 100/50/25", overview.StatusBars[0].WidthPercent, overview.StatusBars[1].WidthPercent, overview.StatusBars[2].WidthPercent)
|
|
}
|
|
}
|
|
|
|
func TestAdminOverviewRendersInlineBarDimensions(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
adminToken := createAdminSession(t, app)
|
|
uploadThroughApp(t, app)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/admin", nil)
|
|
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
|
response := httptest.NewRecorder()
|
|
app.AdminDashboard(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("AdminDashboard status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
body := response.Body.String()
|
|
if !strings.Contains(body, `style="height: 150px"`) {
|
|
t.Fatalf("admin overview did not render a full height pixel bar: %s", body)
|
|
}
|
|
if !strings.Contains(body, `data-height-px="150"`) || !strings.Contains(body, `data-chart-value=`) {
|
|
t.Fatalf("admin overview did not render chart fallback data attributes: %s", body)
|
|
}
|
|
if !strings.Contains(body, `style="height: 0px"`) {
|
|
t.Fatalf("admin overview did not render zero pixel bars: %s", body)
|
|
}
|
|
if !strings.Contains(body, `style="width: 100%"`) {
|
|
t.Fatalf("admin overview did not render a full-width status bar: %s", body)
|
|
}
|
|
if !strings.Contains(body, `data-width-percent="100"`) || !strings.Contains(body, `data-stat-value=`) {
|
|
t.Fatalf("admin overview did not render status fallback data attributes: %s", body)
|
|
}
|
|
if strings.Contains(body, "--bar-height") {
|
|
t.Fatalf("admin overview still uses css variable bar heights: %s", body)
|
|
}
|
|
if !strings.Contains(body, "/static/js/25-admin-charts.js?version=test") {
|
|
t.Fatalf("admin overview did not load chart fallback script: %s", body)
|
|
}
|
|
}
|
|
|
|
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 TestAdminStorageDeleteAction(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
adminToken := createAdminSession(t, app)
|
|
cfg, err := app.uploadService.Storage().CreateBackend(services.StorageBackendConfig{
|
|
Provider: services.StorageProviderWebDAV,
|
|
Name: "DAV",
|
|
Endpoint: "https://dav.example.test",
|
|
Username: "warpbox",
|
|
Password: "secret",
|
|
RemotePath: "/warpbox",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateBackend returned error: %v", err)
|
|
}
|
|
|
|
deleteRequest := httptest.NewRequest(http.MethodPost, "/admin/storage/"+cfg.ID+"/delete", strings.NewReader("csrf_token=test-csrf"))
|
|
deleteRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
deleteRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
|
deleteRequest.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
|
|
deleteRequest.SetPathValue("backendID", cfg.ID)
|
|
deleteResponse := httptest.NewRecorder()
|
|
app.AdminDeleteStorage(deleteResponse, deleteRequest)
|
|
if deleteResponse.Code != http.StatusSeeOther {
|
|
t.Fatalf("AdminDeleteStorage status = %d, body = %s", deleteResponse.Code, deleteResponse.Body.String())
|
|
}
|
|
if _, err := app.uploadService.Storage().BackendConfig(cfg.ID); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("BackendConfig after delete = %v, want not exist", err)
|
|
}
|
|
}
|
|
|
|
func TestAdminStorageDeleteResetsDefaultsAndUserOverrides(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
adminToken := createAdminSession(t, app)
|
|
user, err := app.authService.UserByEmail("admin@example.test")
|
|
if err != nil {
|
|
t.Fatalf("UserByEmail returned error: %v", err)
|
|
}
|
|
cfg, err := app.uploadService.Storage().CreateBackend(services.StorageBackendConfig{
|
|
Provider: services.StorageProviderWebDAV,
|
|
Name: "DAV",
|
|
Endpoint: "https://dav.example.test",
|
|
Username: "warpbox",
|
|
Password: "secret",
|
|
RemotePath: "/warpbox",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateBackend returned error: %v", err)
|
|
}
|
|
settings, err := app.settingsService.UploadPolicy()
|
|
if err != nil {
|
|
t.Fatalf("UploadPolicy returned error: %v", err)
|
|
}
|
|
settings.UserStorageBackend = cfg.ID
|
|
if err := app.settingsService.UpdateUploadPolicy(settings); err != nil {
|
|
t.Fatalf("UpdateUploadPolicy returned error: %v", err)
|
|
}
|
|
if err := app.authService.SetUserStorageBackend(user.ID, cfg.ID); err != nil {
|
|
t.Fatalf("SetUserStorageBackend returned error: %v", err)
|
|
}
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/admin/storage/"+cfg.ID+"/delete", 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"})
|
|
request.SetPathValue("backendID", cfg.ID)
|
|
response := httptest.NewRecorder()
|
|
app.AdminDeleteStorage(response, request)
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("AdminDeleteStorage status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
location := response.Header().Get("Location")
|
|
if !strings.Contains(location, "Storage+backend+deleted") || !strings.Contains(location, "cleared+1+user+overrides") {
|
|
t.Fatalf("delete redirect did not include cascade notice: %s", location)
|
|
}
|
|
if _, err := app.uploadService.Storage().BackendConfig(cfg.ID); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("BackendConfig after delete = %v, want not exist", err)
|
|
}
|
|
nextSettings, err := app.settingsService.UploadPolicy()
|
|
if err != nil {
|
|
t.Fatalf("UploadPolicy returned error: %v", err)
|
|
}
|
|
if nextSettings.UserStorageBackend != services.StorageBackendLocal {
|
|
t.Fatalf("UserStorageBackend = %q, want local", nextSettings.UserStorageBackend)
|
|
}
|
|
nextUser, err := app.authService.UserByID(user.ID)
|
|
if err != nil {
|
|
t.Fatalf("UserByID returned error: %v", err)
|
|
}
|
|
if nextUser.Policy.StorageBackendID != nil {
|
|
t.Fatalf("user storage override was not cleared: %+v", nextUser.Policy)
|
|
}
|
|
}
|
|
|
|
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 TestAdminLogsAndBansPagesRender(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
adminToken := createAdminSession(t, app)
|
|
logDir := filepath.Join(app.cfg.DataDir, "logs")
|
|
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
|
t.Fatalf("MkdirAll returned error: %v", err)
|
|
}
|
|
logPath := filepath.Join(logDir, "2026-05-31.log")
|
|
lines := strings.Join([]string{
|
|
`{"date":"2026-05-31","time":"12:34:56","source":"user-upload","severity":"user_activity","code":2001,"log":"upload response sent","ip":"127.0.0.1","box_id":"box123"}`,
|
|
`{"date":"2026-05-31","time":"12:35:56","source":"http","severity":"dev","code":200,"log":"http request","remote_addr":"172.30.0.1:48358","box_id":"box456"}`,
|
|
`{"date":"2026-05-31","time":"12:36:56","source":"http","severity":"dev","code":200,"log":"http request","method":"GET","path":"/health","ip":"127.0.0.1","user_agent":"Wget"}`,
|
|
"",
|
|
}, "\n")
|
|
if err := os.WriteFile(logPath, []byte(lines), 0o644); err != nil {
|
|
t.Fatalf("WriteFile returned error: %v", err)
|
|
}
|
|
|
|
logsRequest := httptest.NewRequest(http.MethodGet, "/admin/logs?q=box123", nil)
|
|
logsRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
|
logsResponse := httptest.NewRecorder()
|
|
app.AdminLogs(logsResponse, logsRequest)
|
|
if logsResponse.Code != http.StatusOK {
|
|
t.Fatalf("AdminLogs status = %d, body = %s", logsResponse.Code, logsResponse.Body.String())
|
|
}
|
|
logsBody := logsResponse.Body.String()
|
|
if !strings.Contains(logsBody, "upload response sent") || !strings.Contains(logsBody, "box123") {
|
|
t.Fatalf("AdminLogs missing expected log entry: %s", logsBody)
|
|
}
|
|
if strings.Contains(logsBody, "172.30.0.1:48358") {
|
|
t.Fatalf("AdminLogs rendered remote address with port: %s", logsBody)
|
|
}
|
|
healthRequest := httptest.NewRequest(http.MethodGet, "/admin/logs", nil)
|
|
healthRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
|
healthResponse := httptest.NewRecorder()
|
|
app.AdminLogs(healthResponse, healthRequest)
|
|
if healthResponse.Code != http.StatusOK {
|
|
t.Fatalf("AdminLogs health status = %d, body = %s", healthResponse.Code, healthResponse.Body.String())
|
|
}
|
|
if strings.Contains(healthResponse.Body.String(), "/health") || strings.Contains(healthResponse.Body.String(), "Wget") {
|
|
t.Fatalf("AdminLogs rendered container health ping: %s", healthResponse.Body.String())
|
|
}
|
|
|
|
bansRequest := httptest.NewRequest(http.MethodGet, "/admin/bans", nil)
|
|
bansRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
|
bansResponse := httptest.NewRecorder()
|
|
app.AdminBans(bansResponse, bansRequest)
|
|
if bansResponse.Code != http.StatusOK {
|
|
t.Fatalf("AdminBans status = %d, body = %s", bansResponse.Code, bansResponse.Body.String())
|
|
}
|
|
if !strings.Contains(bansResponse.Body.String(), "Manual ban") || !strings.Contains(bansResponse.Body.String(), "Auto-ban settings") {
|
|
t.Fatalf("AdminBans missing ban controls: %s", bansResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAdminCanCreateAndUnbanIPBan(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
adminToken := createAdminSession(t, app)
|
|
expiresAt := time.Now().Add(24 * time.Hour).Format("2006-01-02T15:04")
|
|
request := httptest.NewRequest(http.MethodPost, "/admin/bans", strings.NewReader("target=203.0.113.90&reason=test&expires_at="+expiresAt+"&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.AdminCreateBan(response, request)
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("AdminCreateBan status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
records, err := app.banService.ListBans()
|
|
if err != nil {
|
|
t.Fatalf("ListBans returned error: %v", err)
|
|
}
|
|
if len(records) != 1 || records[0].Normalized != "203.0.113.90" {
|
|
t.Fatalf("records = %+v", records)
|
|
}
|
|
|
|
unbanRequest := httptest.NewRequest(http.MethodPost, "/admin/bans/"+records[0].ID+"/unban", strings.NewReader("csrf_token=test-csrf"))
|
|
unbanRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
unbanRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
|
unbanRequest.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
|
|
unbanRequest.SetPathValue("banID", records[0].ID)
|
|
unbanResponse := httptest.NewRecorder()
|
|
app.AdminUnban(unbanResponse, unbanRequest)
|
|
if unbanResponse.Code != http.StatusSeeOther {
|
|
t.Fatalf("AdminUnban status = %d, body = %s", unbanResponse.Code, unbanResponse.Body.String())
|
|
}
|
|
if _, ok, err := app.banService.Match("203.0.113.90", time.Now().UTC()); err != nil || ok {
|
|
t.Fatalf("unbanned Match = %v, %v", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestAdminCanUpdateBanSettingsAndRules(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
adminToken := createAdminSession(t, app)
|
|
settingsRequest := httptest.NewRequest(http.MethodPost, "/admin/bans/settings", strings.NewReader("auto_ban_enabled=on&auto_ban_duration_hours=48&abuse_window_hours=12&malicious_path_threshold=2&admin_login_failure_threshold=4&user_login_failure_threshold=5&csrf_token=test-csrf"))
|
|
settingsRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
settingsRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
|
settingsRequest.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
|
|
settingsResponse := httptest.NewRecorder()
|
|
app.AdminBanSettingsPost(settingsResponse, settingsRequest)
|
|
if settingsResponse.Code != http.StatusSeeOther {
|
|
t.Fatalf("AdminBanSettingsPost status = %d, body = %s", settingsResponse.Code, settingsResponse.Body.String())
|
|
}
|
|
settings, err := app.banService.Settings()
|
|
if err != nil {
|
|
t.Fatalf("Settings returned error: %v", err)
|
|
}
|
|
if !settings.AutoBanEnabled || settings.AutoBanDurationHours != 48 || settings.MaliciousPathThreshold != 2 {
|
|
t.Fatalf("settings = %+v", settings)
|
|
}
|
|
|
|
rulesRequest := httptest.NewRequest(http.MethodPost, "/admin/bans/rules", strings.NewReader("patterns=%2Fcustom-one%0A%2Fcustom-two&csrf_token=test-csrf"))
|
|
rulesRequest.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
rulesRequest.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: adminToken})
|
|
rulesRequest.AddCookie(&http.Cookie{Name: csrfCookieName, Value: "test-csrf"})
|
|
rulesResponse := httptest.NewRecorder()
|
|
app.AdminBanRulesPost(rulesResponse, rulesRequest)
|
|
if rulesResponse.Code != http.StatusSeeOther {
|
|
t.Fatalf("AdminBanRulesPost status = %d, body = %s", rulesResponse.Code, rulesResponse.Body.String())
|
|
}
|
|
if pattern, err := app.banService.MaliciousPattern("/x/custom-two"); err != nil || pattern != "/custom-two" {
|
|
t.Fatalf("MaliciousPattern = %q, %v", pattern, err)
|
|
}
|
|
}
|
|
|
|
func TestLoginFailuresCreateAutoBanWhenEnabled(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
_, err := app.authService.CreateBootstrapUser("admin", "admin@example.test", "password123")
|
|
if err != nil {
|
|
t.Fatalf("CreateBootstrapUser returned error: %v", err)
|
|
}
|
|
settings, err := app.banService.Settings()
|
|
if err != nil {
|
|
t.Fatalf("Settings returned error: %v", err)
|
|
}
|
|
settings.AutoBanEnabled = true
|
|
settings.UserLoginFailureThreshold = 2
|
|
if err := app.banService.UpdateSettings(settings); err != nil {
|
|
t.Fatalf("UpdateSettings returned error: %v", err)
|
|
}
|
|
|
|
for i := 0; i < 2; i++ {
|
|
request := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader("email=admin@example.test&password=wrong"))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.RemoteAddr = "203.0.113.91:1234"
|
|
response := httptest.NewRecorder()
|
|
app.LoginPost(response, request)
|
|
if response.Code != http.StatusUnauthorized {
|
|
t.Fatalf("LoginPost status = %d", response.Code)
|
|
}
|
|
}
|
|
if _, ok, err := app.banService.Match("203.0.113.91", time.Now().UTC()); err != nil || !ok {
|
|
t.Fatalf("Match after login failures = %v, %v", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestAdminLoginFailuresCreateAutoBanWhenEnabled(t *testing.T) {
|
|
app, cleanup := newTestApp(t)
|
|
defer cleanup()
|
|
|
|
settings, err := app.banService.Settings()
|
|
if err != nil {
|
|
t.Fatalf("Settings returned error: %v", err)
|
|
}
|
|
settings.AutoBanEnabled = true
|
|
settings.AdminLoginFailureThreshold = 2
|
|
if err := app.banService.UpdateSettings(settings); err != nil {
|
|
t.Fatalf("UpdateSettings returned error: %v", err)
|
|
}
|
|
app.cfg.AdminToken = "correct-token"
|
|
|
|
for i := 0; i < 2; i++ {
|
|
request := httptest.NewRequest(http.MethodPost, "/admin/login", strings.NewReader("token=wrong"))
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
request.RemoteAddr = "203.0.113.92:1234"
|
|
response := httptest.NewRecorder()
|
|
app.AdminLoginPost(response, request)
|
|
if response.Code != http.StatusUnauthorized {
|
|
t.Fatalf("AdminLoginPost status = %d", response.Code)
|
|
}
|
|
}
|
|
if _, ok, err := app.banService.Match("203.0.113.92", time.Now().UTC()); err != nil || !ok {
|
|
t.Fatalf("Match after admin login failures = %v, %v", ok, err)
|
|
}
|
|
}
|
|
|
|
func createOwnedBoxThroughApp(t *testing.T, app *App, userID string) services.UploadResult {
|
|
t.Helper()
|
|
user, err := app.authService.UserByID(userID)
|
|
if err != nil {
|
|
t.Fatalf("UserByID returned error: %v", err)
|
|
}
|
|
_, token, err := app.authService.Login(user.Email, "password123")
|
|
if err != nil {
|
|
t.Fatalf("Login returned error: %v", err)
|
|
}
|
|
request := multipartUploadRequest(t, "/api/v1/upload", "file", "owned.txt", "owned")
|
|
request.Header.Set("Accept", "application/json")
|
|
request.AddCookie(&http.Cookie{Name: userSessionCookieName, Value: token})
|
|
response := httptest.NewRecorder()
|
|
app.Upload(response, request)
|
|
if response.Code != http.StatusCreated {
|
|
t.Fatalf("upload status = %d, body = %s", response.Code, response.Body.String())
|
|
}
|
|
var payload services.UploadResult
|
|
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("json.Unmarshal returned error: %v", err)
|
|
}
|
|
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()
|
|
if err != nil {
|
|
t.Fatalf("UploadPolicy returned error: %v", err)
|
|
}
|
|
return policy
|
|
}
|