package services import ( "bytes" "context" "io" "log/slog" "mime/multipart" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" ) func TestDeleteTokenVerification(t *testing.T) { service := newTestUploadService(t) result := createTestBox(t, service, "file.txt", "hello") box := getTestBox(t, service, result.BoxID) token := tokenFromManageURL(t, result.ManageURL) if box.DeleteTokenHash == "" { t.Fatalf("DeleteTokenHash was not stored") } if strings.Contains(box.DeleteTokenHash, token) { t.Fatalf("DeleteTokenHash contains the raw token") } if !service.VerifyDeleteToken(box, token) { t.Fatalf("VerifyDeleteToken rejected the correct token") } if service.VerifyDeleteToken(box, "wrong-token") { t.Fatalf("VerifyDeleteToken accepted the wrong token") } } func TestDeleteBoxWithTokenRemovesMetadataAndFiles(t *testing.T) { service := newTestUploadService(t) result := createTestBox(t, service, "file.txt", "hello") box := getTestBox(t, service, result.BoxID) token := tokenFromManageURL(t, result.ManageURL) if _, err := os.Stat(filepath.Join(service.filesDir, box.ID)); err != nil { t.Fatalf("box files were not created: %v", err) } if err := service.DeleteBoxWithToken(box.ID, "wrong-token"); err == nil { t.Fatalf("DeleteBoxWithToken accepted the wrong token") } if _, err := service.GetBox(box.ID); err != nil { t.Fatalf("box was deleted after wrong token: %v", err) } if err := service.DeleteBoxWithToken(box.ID, token); err != nil { t.Fatalf("DeleteBoxWithToken returned error: %v", err) } if _, err := service.GetBox(box.ID); !os.IsNotExist(err) { t.Fatalf("GetBox after delete error = %v, want os.ErrNotExist", err) } if _, err := os.Stat(filepath.Join(service.filesDir, box.ID)); !os.IsNotExist(err) { t.Fatalf("box directory still exists after delete: %v", err) } } func TestUserActiveStorageUsedIgnoresExpiredBoxes(t *testing.T) { service := newTestUploadService(t) active, err := service.CreateBox(testFileHeaders(t, "file", "active.txt", "active"), UploadOptions{MaxDays: 1, OwnerID: "user-1"}) if err != nil { t.Fatalf("CreateBox active returned error: %v", err) } expired, err := service.CreateBox(testFileHeaders(t, "file", "expired.txt", "expired"), UploadOptions{MaxDays: 1, OwnerID: "user-1"}) if err != nil { t.Fatalf("CreateBox expired returned error: %v", err) } expiredBox, err := service.GetBox(expired.BoxID) if err != nil { t.Fatalf("GetBox returned error: %v", err) } expiredBox.ExpiresAt = time.Now().UTC().Add(-time.Hour) if err := service.SaveBox(expiredBox); err != nil { t.Fatalf("SaveBox returned error: %v", err) } activeBox, err := service.GetBox(active.BoxID) if err != nil { t.Fatalf("GetBox active returned error: %v", err) } want := activeBox.Files[0].Size got, err := service.UserActiveStorageUsed("user-1") if err != nil { t.Fatalf("UserActiveStorageUsed returned error: %v", err) } if got != want { t.Fatalf("UserActiveStorageUsed = %d, want %d", got, want) } } func TestLocalStorageBackendAndLegacyFallback(t *testing.T) { service := newTestUploadService(t) result := createTestBox(t, service, "file.txt", "hello") box := getTestBox(t, service, result.BoxID) if service.BoxStorageBackendID(box) != StorageBackendLocal { t.Fatalf("BoxStorageBackendID = %q", service.BoxStorageBackendID(box)) } if box.Files[0].ObjectKey == "" { t.Fatalf("new file did not store object key") } object, err := service.OpenFileObject(testContext(), box, box.Files[0]) if err != nil { t.Fatalf("OpenFileObject returned error: %v", err) } data, err := io.ReadAll(object.Body) object.Body.Close() if err != nil { t.Fatalf("ReadAll returned error: %v", err) } if string(data) != "hello" { t.Fatalf("object body = %q", string(data)) } box.StorageBackendID = "" box.Files[0].ObjectKey = "" object, err = service.OpenFileObject(testContext(), box, box.Files[0]) if err != nil { t.Fatalf("legacy OpenFileObject returned error: %v", err) } object.Body.Close() } func TestContaboStorageConfigAllowsDisplayNamesWithSpaces(t *testing.T) { service := newTestUploadService(t) cfg, err := service.Storage().CreateS3Backend(StorageBackendConfig{ Provider: StorageProviderContabo, Name: "Contabo main", Endpoint: "https://eu2.contabostorage.com", Region: "EU", Bucket: "My Main Bucket", AccessKey: "access", SecretKey: "secret", }) if err != nil { t.Fatalf("CreateS3Backend returned error: %v", err) } if cfg.Provider != StorageProviderContabo || !cfg.UseSSL || !cfg.PathStyle { t.Fatalf("contabo config was not normalized: %+v", cfg) } if cfg.Bucket != "My Main Bucket" { t.Fatalf("bucket = %q", cfg.Bucket) } } func TestSFTPStorageConfigValidation(t *testing.T) { service := newTestUploadService(t) cfg, err := service.Storage().CreateS3Backend(StorageBackendConfig{ Provider: StorageProviderSFTP, Name: "NAS storage", Host: "files.example.test", Username: "warpbox", Password: "secret", RemotePath: "/srv/warpbox//", }) if err != nil { t.Fatalf("CreateS3Backend returned error: %v", err) } if cfg.Type != StorageBackendSFTP || cfg.Provider != StorageProviderSFTP { t.Fatalf("sftp config type/provider = %+v", cfg) } if cfg.Port != 22 { t.Fatalf("port = %d, want 22", cfg.Port) } if cfg.RemotePath != "/srv/warpbox" { t.Fatalf("remote path = %q", cfg.RemotePath) } } func TestStorageUpdateRejectsProviderMutation(t *testing.T) { service := newTestUploadService(t) cfg, err := service.Storage().CreateBackend(StorageBackendConfig{ Provider: StorageProviderSFTP, Name: "SFTP", Host: "files.example.test", Username: "warpbox", Password: "secret", }) if err != nil { t.Fatalf("CreateBackend returned error: %v", err) } if _, err := service.Storage().UpdateBackend(cfg.ID, StorageBackendConfig{ Provider: StorageProviderS3, Name: "Mutated", Endpoint: "https://s3.example.test", Bucket: "bucket", AccessKey: "access", SecretKey: "secret", UseSSL: true, }); err == nil { t.Fatalf("UpdateBackend allowed provider mutation") } stored, err := service.Storage().BackendConfig(cfg.ID) if err != nil { t.Fatalf("BackendConfig returned error: %v", err) } if stored.Provider != StorageProviderSFTP || stored.Type != StorageBackendSFTP { t.Fatalf("provider/type mutated despite error: %+v", stored) } if _, err := service.Storage().UpdateBackend(cfg.ID, StorageBackendConfig{ Provider: StorageProviderSFTP, Type: StorageBackendS3, Name: "Mutated", Host: "files.example.test", Username: "warpbox", Password: "secret", }); err == nil { t.Fatalf("UpdateBackend allowed type mutation") } } func TestStorageUpdatePreservesSecretsWhenBlank(t *testing.T) { service := newTestUploadService(t) cfg, err := service.Storage().CreateBackend(StorageBackendConfig{ Provider: StorageProviderSFTP, Name: "SFTP", Host: "files.example.test", Username: "warpbox", Password: "secret", PrivateKey: "private-key", HostKey: "host-key", }) if err != nil { t.Fatalf("CreateBackend returned error: %v", err) } updated, err := service.Storage().UpdateBackend(cfg.ID, StorageBackendConfig{ Provider: StorageProviderSFTP, Name: "SFTP renamed", Host: "files.example.test", Username: "warpbox", }) if err != nil { t.Fatalf("UpdateBackend returned error: %v", err) } if updated.Password != "secret" || updated.PrivateKey != "private-key" || updated.HostKey != "host-key" { t.Fatalf("blank secret fields were not preserved: %+v", updated) } } func TestContaboUpdateKeepsTLSAndPathStyleLocked(t *testing.T) { service := newTestUploadService(t) cfg, err := service.Storage().CreateBackend(StorageBackendConfig{ Provider: StorageProviderContabo, Name: "Contabo", Endpoint: "https://eu2.contabostorage.com", Bucket: "My Main Bucket", AccessKey: "access", SecretKey: "secret", }) if err != nil { t.Fatalf("CreateBackend returned error: %v", err) } updated, err := service.Storage().UpdateBackend(cfg.ID, StorageBackendConfig{ Provider: StorageProviderContabo, Name: "Contabo", Endpoint: "https://eu2.contabostorage.com", Bucket: "My Main Bucket", AccessKey: "access", SecretKey: "secret", UseSSL: false, PathStyle: false, }) if err != nil { t.Fatalf("UpdateBackend returned error: %v", err) } if !updated.UseSSL || !updated.PathStyle { t.Fatalf("contabo TLS/path-style were not locked: %+v", updated) } } func TestStorageSpeedTestRequiresConnectionAndRuns(t *testing.T) { service := newTestUploadService(t) if _, err := service.Storage().StartSpeedTest(StorageBackendLocal, StorageSpeedModeSmall); err == nil { t.Fatalf("StartSpeedTest allowed speed test before connection test") } if _, err := service.Storage().TestBackend(StorageBackendLocal); err != nil { t.Fatalf("TestBackend local returned error: %v", err) } test, err := service.Storage().StartSpeedTest(StorageBackendLocal, StorageSpeedModeSmall) if err != nil { t.Fatalf("StartSpeedTest returned error: %v", err) } service.Storage().RunSpeedTest(testContext(), test.ID) tests, err := service.Storage().ListSpeedTests(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)) } got := tests[0] if got.Status != StorageSpeedStatusDone || got.ProgressPercent != 100 || got.Stage != "complete" || got.BytesWritten == 0 || got.BytesRead == 0 || got.FilesWritten == 0 { t.Fatalf("speed test did not complete with metrics: %+v", got) } } func TestCustomStorageSpeedTestUsesRequestedFiles(t *testing.T) { service := newTestUploadService(t) if _, err := service.Storage().TestBackend(StorageBackendLocal); err != nil { t.Fatalf("TestBackend local returned error: %v", err) } test, err := service.Storage().StartSpeedTestWithOptions(StorageBackendLocal, StorageSpeedTestOptions{ Mode: StorageSpeedModeCustom, CustomFileCount: 3, CustomFileSizeMB: 0.001, }) if err != nil { t.Fatalf("StartSpeedTestWithOptions returned error: %v", err) } service.Storage().RunSpeedTest(testContext(), test.ID) tests, err := service.Storage().ListSpeedTests(StorageBackendLocal, 10) if err != nil { t.Fatalf("ListSpeedTests returned error: %v", err) } got := tests[0] if got.Mode != StorageSpeedModeCustom || got.CustomFileCount != 3 || got.CustomFileSizeMB != 0.001 || got.FilesWritten != 3 || got.Status != StorageSpeedStatusDone { t.Fatalf("custom speed test did not use requested files: %+v", got) } } func TestSMBAndWebDAVStorageConfigValidation(t *testing.T) { service := newTestUploadService(t) smb, err := service.Storage().CreateS3Backend(StorageBackendConfig{ Provider: StorageProviderSMB, Name: "Office NAS", Host: "nas.example.test", Username: "warpbox", Password: "secret", Share: "uploads", RemotePath: "/warpbox//", }) if err != nil { t.Fatalf("CreateS3Backend smb returned error: %v", err) } if smb.Type != StorageBackendSMB || smb.Provider != StorageProviderSMB || smb.Port != 445 { t.Fatalf("smb config was not normalized: %+v", smb) } if smb.RemotePath != "/warpbox" { t.Fatalf("smb remote path = %q", smb.RemotePath) } webdav, err := service.Storage().CreateS3Backend(StorageBackendConfig{ Provider: StorageProviderWebDAV, Name: "Nextcloud", Endpoint: "https://files.example.test/webdav", Username: "warpbox", Password: "secret", RemotePath: "/warpbox", }) if err != nil { t.Fatalf("CreateS3Backend webdav returned error: %v", err) } if webdav.Type != StorageBackendWebDAV || webdav.Provider != StorageProviderWebDAV { t.Fatalf("webdav config was not normalized: %+v", webdav) } } func testContext() context.Context { return context.Background() } func newTestUploadService(t *testing.T) *UploadService { t.Helper() service, err := NewUploadService(1024*1024, t.TempDir(), "http://example.test", slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatalf("NewUploadService returned error: %v", err) } t.Cleanup(func() { if err := service.Close(); err != nil { t.Fatalf("Close returned error: %v", err) } }) return service } func createTestBox(t *testing.T, service *UploadService, filename, body string) UploadResult { t.Helper() result, err := service.CreateBox(testFileHeaders(t, "file", filename, body), UploadOptions{MaxDays: 1}) if err != nil { t.Fatalf("CreateBox returned error: %v", err) } return result } func getTestBox(t *testing.T, service *UploadService, boxID string) Box { t.Helper() box, err := service.GetBox(boxID) if err != nil { t.Fatalf("GetBox returned error: %v", err) } return box } func testFileHeaders(t *testing.T, field, filename, body string) []*multipart.FileHeader { t.Helper() var payload bytes.Buffer writer := multipart.NewWriter(&payload) part, err := writer.CreateFormFile(field, filename) if err != nil { t.Fatalf("CreateFormFile returned error: %v", err) } if _, err := part.Write([]byte(body)); err != nil { t.Fatalf("part.Write returned error: %v", err) } if err := writer.Close(); err != nil { t.Fatalf("writer.Close returned error: %v", err) } request := httptest.NewRequest("POST", "/upload", &payload) request.Header.Set("Content-Type", writer.FormDataContentType()) if err := request.ParseMultipartForm(1024 * 1024); err != nil { t.Fatalf("ParseMultipartForm returned error: %v", err) } return request.MultipartForm.File[field] } func tokenFromManageURL(t *testing.T, manageURL string) string { t.Helper() parts := strings.Split(strings.TrimRight(manageURL, "/"), "/") if len(parts) == 0 { t.Fatalf("empty manage URL") } return parts[len(parts)-1] }