Files
warpbox-dev/backend/libs/services/upload_test.go
Daniel Legt 17c31be8b4
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m47s
chore(test): Fixed test
2026-06-02 22:28:35 +03:00

686 lines
24 KiB
Go

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 TestResumableSessionUploadOutOfOrderAndComplete(t *testing.T) {
service := newTestUploadService(t)
session, err := service.CreateResumableSession([]ResumableFileInput{{
Name: "note.txt",
Size: 11,
ContentType: "text/plain",
Fingerprint: "sha256:first-chunk",
}}, UploadOptions{MaxDays: 1, Password: "secret"}, 4, time.Hour, "")
if err != nil {
t.Fatalf("CreateResumableSession returned error: %v", err)
}
if session.ResumeToken == "" || session.ResumeTokenHash == "" {
t.Fatalf("resumable session did not create resume token: %+v", session)
}
if !service.VerifyResumableToken(session, session.ResumeToken) {
t.Fatalf("VerifyResumableToken rejected correct token")
}
if service.VerifyResumableToken(session, "wrong-token") {
t.Fatalf("VerifyResumableToken accepted wrong token")
}
stored, err := service.GetResumableSession(session.ID)
if err != nil {
t.Fatalf("GetResumableSession returned error: %v", err)
}
if stored.ResumeToken != "" {
t.Fatalf("stored session leaked raw resume token")
}
if strings.Contains(stored.ResumeTokenHash, session.ResumeToken) {
t.Fatalf("stored token hash contains raw token")
}
if !service.VerifyResumableToken(stored, session.ResumeToken) {
t.Fatalf("stored session rejected correct token")
}
if session.Options.Password != "" || session.Options.PasswordHash == "" || session.Options.PasswordSalt == "" {
t.Fatalf("resumable session did not hash password before storage: %+v", session.Options)
}
if session.Files[0].ChunkCount != 3 {
t.Fatalf("ChunkCount = %d, want 3", session.Files[0].ChunkCount)
}
if session.Files[0].Fingerprint != "sha256:first-chunk" {
t.Fatalf("Fingerprint = %q", session.Files[0].Fingerprint)
}
for index, body := range map[int]string{2: "rld", 0: "hell", 1: "o wo"} {
updated, err := service.PutResumableChunk(testContext(), session.ID, session.Files[0].ID, index, strings.NewReader(body))
if err != nil {
t.Fatalf("PutResumableChunk(%d) returned error: %v", index, err)
}
if len(updated.Files[0].UploadedChunks) == 0 {
t.Fatalf("UploadedChunks was not updated")
}
}
result, completed, err := service.CompleteResumableSession(testContext(), session.ID)
if err != nil {
t.Fatalf("CompleteResumableSession returned error: %v", err)
}
if completed.Status != ResumableStatusCompleted || completed.BoxID != result.BoxID {
t.Fatalf("completed session = %+v, result = %+v", completed, result)
}
box := getTestBox(t, service, result.BoxID)
if box.PasswordHash == "" || box.PasswordSalt == "" || box.PasswordHash != session.Options.PasswordHash {
t.Fatalf("completed box did not preserve hashed password")
}
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 world" {
t.Fatalf("object body = %q", string(data))
}
if _, err := os.Stat(service.resumableSessionDir(session.ID)); !os.IsNotExist(err) {
t.Fatalf("resumable temp dir after complete error = %v, want os.ErrNotExist", err)
}
replayed, replayedSession, err := service.CompleteResumableSession(testContext(), session.ID)
if err != nil {
t.Fatalf("CompleteResumableSession replay returned error: %v", err)
}
if replayed.BoxID != result.BoxID || replayedSession.Status != ResumableStatusCompleted {
t.Fatalf("replayed result = %+v, session = %+v, want box %s completed", replayed, replayedSession, result.BoxID)
}
}
func TestResumableCompleteRejectsMissingChunks(t *testing.T) {
service := newTestUploadService(t)
session, err := service.CreateResumableSession([]ResumableFileInput{{
Name: "note.txt",
Size: 8,
ContentType: "text/plain",
}}, UploadOptions{MaxDays: 1}, 4, time.Hour, "")
if err != nil {
t.Fatalf("CreateResumableSession returned error: %v", err)
}
if _, err := service.PutResumableChunk(testContext(), session.ID, session.Files[0].ID, 0, strings.NewReader("hell")); err != nil {
t.Fatalf("PutResumableChunk returned error: %v", err)
}
if _, _, err := service.CompleteResumableSession(testContext(), session.ID); err == nil {
t.Fatalf("CompleteResumableSession accepted missing chunks")
}
}
func TestResumablePartialCompleteKeepsOnlyFinishedFiles(t *testing.T) {
service := newTestUploadService(t)
session, err := service.CreateResumableSession([]ResumableFileInput{
{Name: "done.txt", Size: 4, ContentType: "text/plain", Fingerprint: "done"},
{Name: "partial.txt", Size: 8, ContentType: "text/plain", Fingerprint: "partial"},
}, UploadOptions{MaxDays: 1}, 4, time.Hour, "")
if err != nil {
t.Fatalf("CreateResumableSession returned error: %v", err)
}
if _, err := service.PutResumableChunk(testContext(), session.ID, session.Files[0].ID, 0, strings.NewReader("done")); err != nil {
t.Fatalf("PutResumableChunk done returned error: %v", err)
}
if _, err := service.PutResumableChunk(testContext(), session.ID, session.Files[1].ID, 0, strings.NewReader("part")); err != nil {
t.Fatalf("PutResumableChunk partial returned error: %v", err)
}
result, completed, err := service.CompleteUploadedResumableSession(testContext(), session.ID)
if err != nil {
t.Fatalf("CompleteUploadedResumableSession returned error: %v", err)
}
if completed.Status != ResumableStatusCompleted || completed.BoxID != result.BoxID || len(completed.Files) != 1 {
t.Fatalf("completed session = %+v, result = %+v", completed, result)
}
box := getTestBox(t, service, result.BoxID)
if len(box.Files) != 1 || box.Files[0].Name != "done.txt" {
t.Fatalf("partial completion box files = %+v", box.Files)
}
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) != "done" {
t.Fatalf("partial completion object = %q", string(data))
}
if _, err := service.GetResumableSession(session.ID); !os.IsNotExist(err) {
t.Fatalf("GetResumableSession after partial complete error = %v, want os.ErrNotExist", err)
}
if _, err := os.Stat(service.resumableSessionDir(session.ID)); !os.IsNotExist(err) {
t.Fatalf("resumable temp dir after partial complete error = %v, want os.ErrNotExist", err)
}
}
func TestResumablePartialCompleteRejectsNoFinishedFiles(t *testing.T) {
service := newTestUploadService(t)
session, err := service.CreateResumableSession([]ResumableFileInput{{
Name: "partial.txt",
Size: 8,
ContentType: "text/plain",
}}, UploadOptions{MaxDays: 1}, 4, time.Hour, "")
if err != nil {
t.Fatalf("CreateResumableSession returned error: %v", err)
}
if _, err := service.PutResumableChunk(testContext(), session.ID, session.Files[0].ID, 0, strings.NewReader("part")); err != nil {
t.Fatalf("PutResumableChunk returned error: %v", err)
}
if _, _, err := service.CompleteUploadedResumableSession(testContext(), session.ID); err == nil {
t.Fatalf("CompleteUploadedResumableSession accepted no completed files")
}
if _, err := service.GetResumableSession(session.ID); err != nil {
t.Fatalf("GetResumableSession after failed partial complete returned error: %v", err)
}
}
func TestResumableSessionCanAddFilesBeforeComplete(t *testing.T) {
service := newTestUploadService(t)
session, err := service.CreateResumableSession([]ResumableFileInput{{
Name: "one.txt",
Size: 4,
ContentType: "text/plain",
Fingerprint: "one",
}}, UploadOptions{MaxDays: 1}, 4, time.Hour, "")
if err != nil {
t.Fatalf("CreateResumableSession returned error: %v", err)
}
if _, err := service.PutResumableChunk(testContext(), session.ID, session.Files[0].ID, 0, strings.NewReader("one!")); err != nil {
t.Fatalf("PutResumableChunk one returned error: %v", err)
}
updated, err := service.AddResumableFiles(session.ID, []ResumableFileInput{{
Name: "two.txt",
Size: 4,
ContentType: "text/plain",
Fingerprint: "two",
}})
if err != nil {
t.Fatalf("AddResumableFiles returned error: %v", err)
}
if len(updated.Files) != 2 {
t.Fatalf("files after add = %d, want 2", len(updated.Files))
}
if updated.Files[0].UploadedChunks[0] != 0 {
t.Fatalf("existing uploaded chunk was not preserved: %+v", updated.Files[0])
}
if _, err := service.AddResumableFiles(session.ID, []ResumableFileInput{{
Name: "two.txt",
Size: 4,
ContentType: "text/plain",
Fingerprint: "two",
}}); err != nil {
t.Fatalf("duplicate AddResumableFiles returned error: %v", err)
}
updated, err = service.GetResumableSession(session.ID)
if err != nil {
t.Fatalf("GetResumableSession returned error: %v", err)
}
if len(updated.Files) != 2 {
t.Fatalf("duplicate add changed file count to %d", len(updated.Files))
}
if _, err := service.PutResumableChunk(testContext(), session.ID, updated.Files[1].ID, 0, strings.NewReader("two!")); err != nil {
t.Fatalf("PutResumableChunk two returned error: %v", err)
}
result, _, err := service.CompleteResumableSession(testContext(), session.ID)
if err != nil {
t.Fatalf("CompleteResumableSession returned error: %v", err)
}
box := getTestBox(t, service, result.BoxID)
if len(box.Files) != 2 {
t.Fatalf("completed box file count = %d, want 2", len(box.Files))
}
}
func TestResumableCleanupRemovesExpiredSessionsAndChunks(t *testing.T) {
service := newTestUploadService(t)
session, err := service.CreateResumableSession([]ResumableFileInput{{
Name: "note.txt",
Size: 4,
ContentType: "text/plain",
}}, UploadOptions{MaxDays: 1}, 4, time.Hour, "")
if err != nil {
t.Fatalf("CreateResumableSession returned error: %v", err)
}
if _, err := service.PutResumableChunk(testContext(), session.ID, session.Files[0].ID, 0, strings.NewReader("hell")); err != nil {
t.Fatalf("PutResumableChunk returned error: %v", err)
}
cleaned, err := service.CleanupExpiredResumableSessions(session.ExpiresAt.Add(time.Second))
if err != nil {
t.Fatalf("CleanupExpiredResumableSessions returned error: %v", err)
}
if cleaned != 1 {
t.Fatalf("cleaned = %d, want 1", cleaned)
}
if _, err := service.GetResumableSession(session.ID); !os.IsNotExist(err) {
t.Fatalf("GetResumableSession after cleanup error = %v, want os.ErrNotExist", err)
}
if _, err := os.Stat(service.resumableSessionDir(session.ID)); !os.IsNotExist(err) {
t.Fatalf("resumable temp dir after cleanup error = %v, want os.ErrNotExist", err)
}
}
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]
}