From c52b7c4abf30cc12efc3b6d1ece5322aa190a2bd Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 28 Aug 2026 18:54:23 +0800 Subject: [PATCH] test(upload): move storage task fixtures to in-memory mock --- .../domain/upload/handler/routers_test.go | 29 ++++------- .../domain/upload/shared/test_helpers.go | 22 +++----- .../task/storage_migration_task_test.go | 51 +++++++++---------- backend/plugins/domain/upload/task/tasks.go | 4 +- .../plugins/domain/upload/task/tasks_test.go | 51 ++++++++++++------- backend/plugins/infra/database/postgres.go | 4 +- 6 files changed, 78 insertions(+), 83 deletions(-) diff --git a/backend/plugins/domain/upload/handler/routers_test.go b/backend/plugins/domain/upload/handler/routers_test.go index dfaaa434..22d391e4 100644 --- a/backend/plugins/domain/upload/handler/routers_test.go +++ b/backend/plugins/domain/upload/handler/routers_test.go @@ -349,7 +349,6 @@ func TestUploadFile(t *testing.T) { func TestDownloadFile(t *testing.T) { dbConn, cleanup := shared.SetupTestEnv(t) defer cleanup() - defer func() { _ = os.RemoveAll("uploads") }() authUser := &contracts.UserDTO{ID: 1001, Username: "test_user"} router := setupTestRouter(authUser) @@ -366,15 +365,10 @@ func TestDownloadFile(t *testing.T) { Status: models.UploadStatusUsed, } - // Create local file - err := os.MkdirAll("uploads", 0o755) - if err != nil { - t.Fatalf("failed to create directory: %v", err) - } - err = os.WriteFile(localUpload.FilePath, []byte("hello download"), 0o644) - if err != nil { - t.Fatalf("failed to write file: %v", err) - } + // Seed file content into the in-memory mock storage service. + storageSvc := shared.NewMockStorageService() + storageSvc.PutRaw(localUpload.FilePath, []byte("hello download")) + shared.SetStorageService(storageSvc) dbConn.Create(&localUpload) @@ -557,20 +551,15 @@ func TestListFiles(t *testing.T) { func TestBatchDownloadFiles(t *testing.T) { dbConn, cleanup := shared.SetupTestEnv(t) defer cleanup() - defer func() { _ = os.RemoveAll("uploads") }() authUser := &contracts.UserDTO{ID: 1001, Username: "test_user"} router := setupTestRouter(authUser) - // Create and write files locally - err := os.MkdirAll("uploads", 0o755) - if err != nil { - t.Fatalf("failed to create local dir: %v", err) - } - - _ = os.WriteFile("uploads/f1.txt", []byte("file1 content"), 0o644) - _ = os.WriteFile("uploads/f2.txt", []byte("file2 content"), 0o644) - _ = os.WriteFile("uploads/f3.txt", []byte("duplicate name file content"), 0o644) + storageSvc := shared.NewMockStorageService() + storageSvc.PutRaw("uploads/f1.txt", []byte("file1 content")) + storageSvc.PutRaw("uploads/f2.txt", []byte("file2 content")) + storageSvc.PutRaw("uploads/f3.txt", []byte("duplicate name file content")) + shared.SetStorageService(storageSvc) // Seed upload records. Note f2 and f3 have the same FileName "file_a.txt" to trigger name collision resolution. uploads := []models.Upload{ diff --git a/backend/plugins/domain/upload/shared/test_helpers.go b/backend/plugins/domain/upload/shared/test_helpers.go index 3569302b..5230ffce 100644 --- a/backend/plugins/domain/upload/shared/test_helpers.go +++ b/backend/plugins/domain/upload/shared/test_helpers.go @@ -13,8 +13,6 @@ import ( "errors" "fmt" "io" - "os" - "path/filepath" "strings" "sync" "testing" @@ -128,13 +126,16 @@ func (m *MockStorageService) Put(_ context.Context, key string, body io.Reader, return contracts.StoragePutResult{}, err } m.objects[key] = data - if strings.HasPrefix(key, "uploads/") { - _ = os.MkdirAll(filepath.Dir(key), 0o755) - _ = os.WriteFile(key, data, 0o644) - } return contracts.StoragePutResult{Key: key, Bucket: "test-bucket"}, nil } +// PutRaw seeds an object directly into the in-memory store (test helper, no I/O). +func (m *MockStorageService) PutRaw(key string, data []byte) { + m.mu.Lock() + defer m.mu.Unlock() + m.objects[key] = data +} + // Get retrieves an object from mock storage. func (m *MockStorageService) Get(_ context.Context, key string) (*contracts.StorageObject, error) { m.mu.RLock() @@ -148,15 +149,6 @@ func (m *MockStorageService) Get(_ context.Context, key string) (*contracts.Stor ContentType: "application/octet-stream", }, nil } - if f, err := os.Open(key); err == nil { - info, _ := f.Stat() - return &contracts.StorageObject{ - Key: key, - Body: f, - ContentLength: info.Size(), - ContentType: "application/octet-stream", - }, nil - } return nil, gorm.ErrRecordNotFound } diff --git a/backend/plugins/domain/upload/task/storage_migration_task_test.go b/backend/plugins/domain/upload/task/storage_migration_task_test.go index e950246d..a48e150e 100644 --- a/backend/plugins/domain/upload/task/storage_migration_task_test.go +++ b/backend/plugins/domain/upload/task/storage_migration_task_test.go @@ -7,38 +7,35 @@ import ( "Wavelet/core/contracts" "Wavelet/plugins/domain/upload/models" "Wavelet/plugins/domain/upload/shared" + "Wavelet/plugins/domain/upload/storage" + "context" "crypto/sha256" "encoding/hex" "encoding/json" - "os" - "path/filepath" "strings" "testing" - - uploadstorage "Wavelet/plugins/domain/upload/storage" ) func TestMigrationHandlerExecute(t *testing.T) { dbConn, cleanup := shared.SetupTestEnv(t) defer cleanup() - sourceRoot := t.TempDir() - sourcePath := filepath.Join(sourceRoot, "uploads", "test.txt") - if err := os.MkdirAll(filepath.Dir(sourcePath), 0o755); err != nil { - t.Fatalf("MkdirAll(%q) returned error: %v", sourcePath, err) - } - const content = "storage migration" - if err := os.WriteFile(sourcePath, []byte(content), 0o644); err != nil { - t.Fatalf("WriteFile(%q) returned error: %v", sourcePath, err) + ctx := context.Background() + storageSvc, ok := shared.GetStorage(ctx).(*shared.MockStorageService) + if !ok { + t.Fatalf("shared.GetStorage(%T) returned %T, want *shared.MockStorageService", ctx, storageSvc) } - ctx := context.Background() + const sourcePath = "uploads/test.txt" + const content = "storage migration" + storageSvc.PutRaw(sourcePath, []byte(content)) + active := contracts.StorageConfigDTO{ Driver: contracts.StorageDriverLocal, - Local: contracts.LocalStorageConfigDTO{Root: sourceRoot}, + Local: contracts.LocalStorageConfigDTO{Root: "unit"}, } - if err := uploadstorage.SaveActiveConfig(ctx, active); err != nil { + if err := storage.SaveActiveConfig(ctx, active); err != nil { t.Fatalf("SaveActiveConfig() returned error: %v", err) } target := contracts.StorageConfigDTO{ @@ -85,7 +82,7 @@ func TestMigrationHandlerExecute(t *testing.T) { if err := dbConn.First(&migrated, upload.ID).Error; err != nil { t.Fatalf("First(upload) returned error: %v", err) } - current, err := uploadstorage.LoadStorageConfig(ctx) + current, err := storage.LoadStorageConfig(ctx) if err != nil { t.Fatalf("LoadStorageConfig() returned error: %v", err) } @@ -98,30 +95,28 @@ func TestMigrationHandlerExecuteWithHashValidation(t *testing.T) { dbConn, cleanup := shared.SetupTestEnv(t) defer cleanup() - sourceRoot := t.TempDir() - sourcePath := filepath.Join(sourceRoot, "uploads", "test-hash.txt") - if err := os.MkdirAll(filepath.Dir(sourcePath), 0o755); err != nil { - t.Fatalf("MkdirAll(%q) returned error: %v", sourcePath, err) + ctx := context.Background() + storageSvc, ok := shared.GetStorage(ctx).(*shared.MockStorageService) + if !ok { + t.Fatalf("shared.GetStorage(%T) returned %T, want *shared.MockStorageService", ctx, storageSvc) } + + const sourcePath = "uploads/test-hash.txt" const content = "storage migration integrity check content" - if err := os.WriteFile(sourcePath, []byte(content), 0o644); err != nil { - t.Fatalf("WriteFile(%q) returned error: %v", sourcePath, err) - } + storageSvc.PutRaw(sourcePath, []byte(content)) // Calculate correct SHA-256 hash h := sha256.New() h.Write([]byte(content)) correctHash := hex.EncodeToString(h.Sum(nil)) - ctx := context.Background() active := contracts.StorageConfigDTO{ Driver: contracts.StorageDriverLocal, - Local: contracts.LocalStorageConfigDTO{Root: sourceRoot}, + Local: contracts.LocalStorageConfigDTO{Root: "unit"}, } - if err := uploadstorage.SaveActiveConfig(ctx, active); err != nil { + if err := storage.SaveActiveConfig(ctx, active); err != nil { t.Fatalf("SaveActiveConfig() returned error: %v", err) } - target := contracts.StorageConfigDTO{ Driver: contracts.StorageDriverS3, S3: contracts.ObjectStorageConfigDTO{ @@ -200,7 +195,7 @@ func TestMigrationHandlerExecuteWithLock(t *testing.T) { active := contracts.StorageConfigDTO{ Driver: contracts.StorageDriverLocal, } - if err := uploadstorage.SaveActiveConfig(ctx, active); err != nil { + if err := storage.SaveActiveConfig(ctx, active); err != nil { t.Fatalf("SaveActiveConfig() returned error: %v", err) } diff --git a/backend/plugins/domain/upload/task/tasks.go b/backend/plugins/domain/upload/task/tasks.go index 86f4dccc..43ec9ae6 100644 --- a/backend/plugins/domain/upload/task/tasks.go +++ b/backend/plugins/domain/upload/task/tasks.go @@ -19,8 +19,8 @@ import ( // 上传域任务的通用元数据常量(TaskMetaDTO Category/Queue 复用) const ( - taskCategoryUpload = "upload" - taskQueueDefault = "default" + taskCategoryUpload = "upload" + taskQueueDefault = "default" ) const ( diff --git a/backend/plugins/domain/upload/task/tasks_test.go b/backend/plugins/domain/upload/task/tasks_test.go index df908a3e..c8313622 100644 --- a/backend/plugins/domain/upload/task/tasks_test.go +++ b/backend/plugins/domain/upload/task/tasks_test.go @@ -7,14 +7,13 @@ import ( "Wavelet/plugins/domain/upload/filesrv" "Wavelet/plugins/domain/upload/models" "Wavelet/plugins/domain/upload/shared" + "bytes" "context" "encoding/json" "image" "image/color" "image/png" - "os" - "path/filepath" "testing" "time" @@ -160,11 +159,19 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) { dbConn, cleanup := shared.SetupTestEnv(t) defer cleanup() - testDir := t.TempDir() - firstPath := filepath.Join(testDir, "first.png") - secondPath := filepath.Join(testDir, "second.jpg") - writeTaskTestPNG(t, firstPath, color.RGBA{R: 255, A: 255}) - writeTaskTestPNG(t, secondPath, color.RGBA{G: 255, A: 255}) + ctx := context.Background() + + firstPath := "uploads/first.png" + secondPath := "uploads/second.jpg" + firstData := writeTaskTestPNG(t, color.RGBA{R: 255, A: 255}) + secondData := writeTaskTestPNG(t, color.RGBA{G: 255, A: 255}) + + storageSvc, ok := shared.GetStorage(ctx).(*shared.MockStorageService) + if !ok { + t.Fatalf("shared.GetStorage(%T) returned %T, want *shared.MockStorageService", ctx, storageSvc) + } + storageSvc.PutRaw(firstPath, firstData) + storageSvc.PutRaw(secondPath, secondData) records := []models.Upload{ { @@ -172,8 +179,11 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) { UserID: 1001, FileName: "first.png", FilePath: firstPath, + FileSize: int64(len(firstData)), MimeType: "image/png", Extension: "png", + Hash: "hash1", + Type: "attachment", Status: models.UploadStatusUsed, }, { @@ -181,17 +191,23 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) { UserID: 1001, FileName: "second.jpg", FilePath: secondPath, + FileSize: int64(len(secondData)), MimeType: "application/octet-stream", Extension: "jpg", + Hash: "hash2", + Type: "attachment", Status: models.UploadStatusPending, }, { ID: 4103, UserID: 1001, FileName: "notes.txt", - FilePath: filepath.Join(testDir, "notes.txt"), + FilePath: "uploads/notes.txt", + FileSize: 123, MimeType: "text/plain", Extension: "txt", + Hash: "hash3", + Type: "attachment", Status: models.UploadStatusUsed, }, { @@ -199,15 +215,16 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) { UserID: 1001, FileName: "deleted.png", FilePath: firstPath, + FileSize: int64(len(firstData)), MimeType: "image/png", Extension: "png", + Hash: "hash4", + Type: "attachment", Status: models.UploadStatusDeleted, }, } + for i := range records { - if info, err := os.Stat(records[i].FilePath); err == nil { - records[i].FileSize = info.Size() - } if err := dbConn.Create(&records[i]).Error; err != nil { t.Fatalf("failed to create upload %d: %v", records[i].ID, err) } @@ -216,7 +233,7 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) { handler := &WarmImageCacheHandler{} payload := []byte(`{"quality":"low"}`) - result, err := handler.Execute(context.Background(), payload) + result, err := handler.Execute(ctx, payload) if err != nil { t.Fatalf("Execute(%s) returned error: %v", payload, err) } @@ -242,16 +259,17 @@ func TestWarmImageCacheHandlerExecute(t *testing.T) { } } - secondResult, err := handler.Execute(context.Background(), payload) + secondResult, err := handler.Execute(ctx, payload) if err != nil { t.Fatalf("second Execute(%s) returned error: %v", payload, err) } if secondResult.Message != "图片缓存预热完成,共处理 2 张,生成 0 张,命中 2 张,失败 0 张" { t.Errorf("second Execute() message = %q, want cache-hit summary", secondResult.Message) } + } -func writeTaskTestPNG(t *testing.T, path string, fill color.RGBA) { +func writeTaskTestPNG(t *testing.T, fill color.RGBA) []byte { t.Helper() img := image.NewRGBA(image.Rect(0, 0, 2, 2)) @@ -265,7 +283,6 @@ func writeTaskTestPNG(t *testing.T, path string, fill color.RGBA) { if err := png.Encode(&buf, img); err != nil { t.Fatalf("png.Encode() returned error: %v", err) } - if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil { - t.Fatalf("os.WriteFile(%q) returned error: %v", path, err) - } + + return buf.Bytes() } diff --git a/backend/plugins/infra/database/postgres.go b/backend/plugins/infra/database/postgres.go index 96335918..4e864fe4 100644 --- a/backend/plugins/infra/database/postgres.go +++ b/backend/plugins/infra/database/postgres.go @@ -26,6 +26,8 @@ import ( var db *gorm.DB +const sqliteDirMode = 0o750 + // InitDB 初始化主数据库实例(支持 PostgreSQL / SQLite) func InitDB() (*gorm.DB, error) { if !config.Config.Database.Enabled { @@ -43,7 +45,7 @@ func initSQLite() (*gorm.DB, error) { if sqlitePath != ":memory:" && !strings.HasPrefix(sqlitePath, "file:") { if dir := filepath.Dir(sqlitePath); dir != "" && dir != "." { - if err := os.MkdirAll(dir, 0o755); err != nil { + if err := os.MkdirAll(dir, sqliteDirMode); err != nil { return nil, fmt.Errorf("create sqlite directory %q failed: %w", dir, err) } }