package service import ( "context" "encoding/json" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/config" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/security" ) var ( testPNG = []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', 0, 0, 0, 0} testMP3 = []byte{'I', 'D', '3', 4, 0, 0, 0, 0, 0, 0} ) // --- MIME detection helper tests --- func TestDetectUploadMIMEFromFilename(t *testing.T) { tests := []struct { filename string want string }{ {"photo.png", "image/png"}, {"photo.jpg", "image/jpeg"}, {"photo.jpeg", "image/jpeg"}, {"anim.gif", "image/gif"}, {"modern.webp", "image/webp"}, {"icon.svg", "image/svg+xml"}, {"song.mp3", "audio/mpeg"}, {"audio.ogg", "audio/ogg"}, {"sound.wav", "audio/wav"}, {"clip.webm", "audio/webm"}, {"video.mp4", "video/mp4"}, {"doc.pdf", "application/pdf"}, {"data.csv", "text/csv"}, {"readme.txt", "text/plain"}, {"sheet.xls", "application/vnd.ms-excel"}, {"sheet.xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, {"letter.doc", "application/msword"}, {"letter.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, {"unknown.xyz", "application/octet-stream"}, {"noext", "application/octet-stream"}, } for _, tt := range tests { t.Run(tt.filename, func(t *testing.T) { got := detectUploadMIMEFromFilename(tt.filename) assert.Equal(t, tt.want, got) }) } } func TestCategorizeUploadMIME(t *testing.T) { tests := []struct { mimeType string want string }{ {"image/png", "image"}, {"image/jpeg", "image"}, {"image/gif", "image"}, {"audio/mpeg", "audio"}, {"audio/ogg", "audio"}, {"video/mp4", "video"}, {"application/pdf", "file"}, {"text/csv", "file"}, {"text/plain", "file"}, {"application/octet-stream", ""}, // unsupported category {"application/zip", ""}, // unsupported } for _, tt := range tests { t.Run(tt.mimeType, func(t *testing.T) { got := categorizeUploadMIME(tt.mimeType) assert.Equal(t, tt.want, got) }) } } func TestIsUploadMIMEAllowed(t *testing.T) { // Allowed MIME types per category assert.True(t, isUploadMIMEAllowed("image", "image/png")) assert.True(t, isUploadMIMEAllowed("image", "image/jpeg")) assert.True(t, isUploadMIMEAllowed("image", "image/gif")) assert.True(t, isUploadMIMEAllowed("audio", "audio/mpeg")) assert.True(t, isUploadMIMEAllowed("audio", "audio/ogg")) assert.True(t, isUploadMIMEAllowed("video", "video/mp4")) assert.True(t, isUploadMIMEAllowed("file", "application/pdf")) assert.True(t, isUploadMIMEAllowed("file", "text/csv")) assert.False(t, isUploadMIMEAllowed("file", "application/octet-stream")) // Disallowed MIME types assert.False(t, isUploadMIMEAllowed("image", "audio/mpeg")) // wrong category assert.False(t, isUploadMIMEAllowed("video", "image/png")) // wrong category assert.False(t, isUploadMIMEAllowed("unknown", "image/png")) // unknown category } // --- Service integration tests using SQLite --- func setupUploadServiceTest(t *testing.T) (*gorm.DB, *UploadService, string) { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err, "failed to open test DB") require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, &model.DirectUpload{}), "failed to auto-migrate upload models") account := &model.Account{Name: "Upload Test", Status: "active"} require.NoError(t, db.Create(account).Error) inbox := &model.Inbox{AccountID: account.ID, Name: "Upload Inbox", ChannelType: "web_widget", ChannelConfig: `{"website_token":"test-website"}`, Enabled: true} require.NoError(t, db.Create(inbox).Error) contact := &model.Contact{AccountID: account.ID, Name: "Upload Contact"} require.NoError(t, db.Create(contact).Error) require.NoError(t, db.Create(&model.ContactInbox{ContactID: contact.ID, InboxID: inbox.ID, PubsubToken: "test-widget"}).Error) t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) // Create temp directory for file storage tmpDir := t.TempDir() cfg := &config.Config{ Storage: config.StorageConfig{ Provider: "local", LocalPath: tmpDir, MaxFileSize: 20 * 1024 * 1024, // 20MB }, } directUploadRepo := repository.NewDirectUploadRepo(db) uploadService := NewUploadService(directUploadRepo, cfg). WithWidgetAuth(repository.NewInboxRepo(db), repository.NewContactInboxRepo(db)). WithAccessDB(db) return db, uploadService, tmpDir } // createTestFileHeader builds a real *multipart.FileHeader from content. // It constructs a real HTTP multipart request, parses it, and extracts the FileHeader. func createTestFileHeader(t *testing.T, filename string, content []byte) *multipart.FileHeader { t.Helper() body := &strings.Builder{} writer := multipart.NewWriter(body) part, err := writer.CreateFormFile("file", filename) require.NoError(t, err, "CreateFormFile failed") _, err = part.Write(content) require.NoError(t, err, "write content failed") require.NoError(t, writer.Close(), "close multipart writer failed") req, err := http.NewRequest("POST", "/", strings.NewReader(body.String())) require.NoError(t, err, "create request failed") req.Header.Set("Content-Type", writer.FormDataContentType()) // FormFile returns (multipart.File, *multipart.FileHeader, error) _, fileHeader, err := req.FormFile("file") require.NoError(t, err, "FormFile failed") return fileHeader } func TestUploadService_AccountUpload_Success(t *testing.T) { _, svc, tmpDir := setupUploadServiceTest(t) fileHeader := createTestFileHeader(t, "test_image.png", testPNG) result, svcErr := svc.AccountUpload(context.Background(), 1, AccountUploadRequest{ FileHeader: fileHeader, }) require.NoError(t, svcErr, "AccountUpload should succeed") assert.NotEmpty(t, result.UploadUUID) assert.Equal(t, "test_image.png", result.OriginalName) assert.Equal(t, "image", result.FileType) assert.Equal(t, "image/png", result.MimeType) assert.Equal(t, "pending", result.Status) assert.NotZero(t, result.ExpiresAt) // Verify file was saved on disk files, _ := filepath.Glob(filepath.Join(tmpDir, "account", "1", "*.png")) assert.GreaterOrEqual(t, len(files), 1, "file should be saved on disk") } func TestUploadSharedStorageSurvivesReplicaRecreation(t *testing.T) { _, svc, storagePath := setupUploadServiceTest(t) content := []byte("shared attachment") upload, err := svc.AccountUpload(context.Background(), 1, AccountUploadRequest{ FileHeader: createTestFileHeader(t, "shared.txt", content), }) require.NoError(t, err) for range 2 { replica := gin.New() replica.StaticFS("/uploads", gin.Dir(storagePath, false)) response := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, upload.FileURL, nil) replica.ServeHTTP(response, request) assert.Equal(t, http.StatusOK, response.Code) assert.Equal(t, content, response.Body.Bytes()) } } func TestUploadService_AccountUpload_NoAccountID(t *testing.T) { _, svc, _ := setupUploadServiceTest(t) _, svcErr := svc.AccountUpload(context.Background(), 0, AccountUploadRequest{ FileHeader: nil, }) assert.Error(t, svcErr) assert.Contains(t, svcErr.Error(), "account_id is required") } func TestUploadService_AccountUpload_NoFile(t *testing.T) { _, svc, _ := setupUploadServiceTest(t) _, svcErr := svc.AccountUpload(context.Background(), 1, AccountUploadRequest{ FileHeader: nil, }) assert.Error(t, svcErr) assert.Contains(t, svcErr.Error(), "file is required") } func TestUploadService_WidgetDirectUpload_Success(t *testing.T) { _, svc, tmpDir := setupUploadServiceTest(t) fileHeader := createTestFileHeader(t, "visitor_audio.mp3", testMP3) result, svcErr := svc.WidgetDirectUpload(context.Background(), WidgetDirectUploadRequest{ WebsiteToken: "test-website", AuthToken: "test-widget", FileHeader: fileHeader, }) require.NoError(t, svcErr, "WidgetDirectUpload should succeed") assert.NotEmpty(t, result.UploadUUID) assert.Equal(t, "visitor_audio.mp3", result.OriginalName) assert.Equal(t, "audio", result.FileType) assert.Equal(t, "audio/mpeg", result.MimeType) assert.Equal(t, "pending", result.Status) // Widget uploads should go to "widget_direct" subdirectory files, _ := filepath.Glob(filepath.Join(tmpDir, "widget_direct", "1", "*.mp3")) assert.GreaterOrEqual(t, len(files), 1, "widget file should be saved in widget_direct dir") } func TestUploadService_WidgetDirectUpload_NoFile(t *testing.T) { _, svc, _ := setupUploadServiceTest(t) _, svcErr := svc.WidgetDirectUpload(context.Background(), WidgetDirectUploadRequest{ FileHeader: nil, }) assert.Error(t, svcErr) assert.Contains(t, svcErr.Error(), "file is required") } func TestUploadService_AccountUpload_UnsupportedFileType(t *testing.T) { _, svc, _ := setupUploadServiceTest(t) // .exe gets application/octet-stream MIME via detectUploadMIMEFromFilename // categorizeUploadMIME("application/octet-stream") returns "" (unsupported) // So processUpload should fail with "unsupported file type" fileHeader := createTestFileHeader(t, "malware.exe", []byte("fake exe content")) _, svcErr := svc.AccountUpload(context.Background(), 1, AccountUploadRequest{ FileHeader: fileHeader, }) assert.Error(t, svcErr) assert.Contains(t, svcErr.Error(), "unsupported file type") } // --- Account Direct Upload service tests --- // Reference: Chatwoot POST /api/v1/accounts/:account_id/direct_uploads func TestUploadService_AccountDirectUpload_Success(t *testing.T) { _, svc, tmpDir := setupUploadServiceTest(t) fileHeader := createTestFileHeader(t, "staged_image.png", testPNG) result, svcErr := svc.AccountDirectUpload(context.Background(), 1, AccountDirectUploadRequest{ FileHeader: fileHeader, }) require.NoError(t, svcErr, "AccountDirectUpload should succeed") assert.NotEmpty(t, result.UploadUUID) assert.Equal(t, "staged_image.png", result.OriginalName) assert.Equal(t, "image", result.FileType) assert.Equal(t, "image/png", result.MimeType) assert.Equal(t, "pending", result.Status) assert.NotZero(t, result.ExpiresAt) // Verify file was saved on disk (account-scoped, same subdirectory as AccountUpload) files, _ := filepath.Glob(filepath.Join(tmpDir, "account", "1", "*.png")) assert.GreaterOrEqual(t, len(files), 1, "file should be saved on disk in account/1/") } func TestUploadService_AccountDirectUpload_NoAccountID(t *testing.T) { _, svc, _ := setupUploadServiceTest(t) fileHeader := createTestFileHeader(t, "test.png", testPNG) _, svcErr := svc.AccountDirectUpload(context.Background(), 0, AccountDirectUploadRequest{ FileHeader: fileHeader, }) assert.Error(t, svcErr) assert.Contains(t, svcErr.Error(), "account_id is required") } func TestUploadService_AccountDirectUpload_NoFile(t *testing.T) { _, svc, _ := setupUploadServiceTest(t) _, svcErr := svc.AccountDirectUpload(context.Background(), 1, AccountDirectUploadRequest{ FileHeader: nil, }) assert.Error(t, svcErr) assert.Contains(t, svcErr.Error(), "file is required") } func TestUploadService_AccountDirectUpload_UnsupportedFileType(t *testing.T) { _, svc, _ := setupUploadServiceTest(t) fileHeader := createTestFileHeader(t, "malware.exe", []byte("fake exe content")) _, svcErr := svc.AccountDirectUpload(context.Background(), 1, AccountDirectUploadRequest{ FileHeader: fileHeader, }) assert.Error(t, svcErr) assert.Contains(t, svcErr.Error(), "unsupported file type") } func TestUploadService_RejectsSpoofedMIMEAndSVG(t *testing.T) { _, svc, _ := setupUploadServiceTest(t) for _, test := range []struct { name string filename string content []byte }{ {name: "forged png", filename: "attack.png", content: []byte("")}, {name: "active svg", filename: "attack.svg", content: []byte(``)}, } { t.Run(test.name, func(t *testing.T) { _, err := svc.AccountUpload(context.Background(), 1, AccountUploadRequest{FileHeader: createTestFileHeader(t, test.filename, test.content)}) require.Error(t, err) }) } } func TestUploadService_RejectsTextPlainSVGAndXMLAcrossUploadPaths(t *testing.T) { _, svc, _ := setupUploadServiceTest(t) payloads := map[string][]byte{ "svg": []byte(``), "xml": []byte(``), } paths := map[string]func(*testing.T, []byte) error{ "direct": func(t *testing.T, payload []byte) error { file := createTestFileHeader(t, "attack.txt", payload) file.Header.Set("Content-Type", "text/plain") _, err := svc.AccountDirectUpload(context.Background(), 1, AccountDirectUploadRequest{FileHeader: file}) return err }, "widget": func(t *testing.T, payload []byte) error { file := createTestFileHeader(t, "attack.txt", payload) file.Header.Set("Content-Type", "text/plain") _, err := svc.WidgetDirectUpload(context.Background(), WidgetDirectUploadRequest{ WebsiteToken: "test-website", AuthToken: "test-widget", FileHeader: file, }) return err }, "remote": func(t *testing.T, payload []byte) error { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "text/plain") _, _ = w.Write(payload) })) defer server.Close() svc.fetchClient = security.NewSafeHTTPClient(security.SSRFConfig{MaxRedirects: 3}) _, err := svc.AccountUploadFromURL(context.Background(), 1, server.URL+"/attack.txt") return err }, } for payloadName, payload := range payloads { for pathName, upload := range paths { t.Run(payloadName+"/"+pathName, func(t *testing.T) { require.ErrorContains(t, upload(t, payload), "active markup") }) } } } func TestUploadService_AccountUploadFromURLRejectsPrivateTargets(t *testing.T) { _, svc, _ := setupUploadServiceTest(t) for _, target := range []string{ "http://127.0.0.1/file.png", "http://10.0.0.1/file.png", "http://169.254.169.254/latest/meta-data/iam/security-credentials/", } { _, err := svc.AccountUploadFromURL(context.Background(), 1, target) require.Error(t, err, target) assert.Contains(t, err.Error(), "SSRF", target) } } func TestUploadService_WidgetUploadRequiresValidSession(t *testing.T) { _, svc, _ := setupUploadServiceTest(t) _, err := svc.WidgetDirectUpload(context.Background(), WidgetDirectUploadRequest{ FileHeader: createTestFileHeader(t, "visitor.png", testPNG), }) require.Error(t, err) assert.Contains(t, err.Error(), "website_token is required") } func TestUploadService_CompleteWidgetUploadRejectsTokenAndMIMESpoof(t *testing.T) { db, svc, tmpDir := setupUploadServiceTest(t) content := []byte("") upload := &model.DirectUpload{ UploadUUID: "secured-widget-upload", AccountID: 1, Status: model.DirectUploadStatusPending, Source: model.DirectUploadSourceWidget, OriginalName: "attack.png", FileType: "image", MimeType: "image/png", FileSize: int64(len(content)), FileURL: "/uploads/widget_direct/secured-widget-upload.png", Metadata: []byte(`{"active_storage_key":"secret-token","contact_inbox_id":1}`), ExpiresAt: time.Now().Add(time.Hour), } require.NoError(t, db.Create(upload).Error) _, err := svc.CompleteWidgetDirectUpload(context.Background(), upload.UploadUUID, strings.NewReader(string(content)), "wrong-token") require.ErrorContains(t, err, "invalid direct upload token") _, err = svc.CompleteWidgetDirectUpload(context.Background(), upload.UploadUUID, strings.NewReader(string(content)), "secret-token") require.ErrorContains(t, err, "does not match") _, statErr := os.Stat(filepath.Join(tmpDir, "widget_direct", "secured-widget-upload.png")) require.ErrorIs(t, statErr, os.ErrNotExist) } func TestUploadService_PrivateAttachmentAccessIsTenantAndSessionScoped(t *testing.T) { db, svc, tmpDir := setupUploadServiceTest(t) require.NoError(t, db.AutoMigrate(&model.Conversation{}, &model.Message{}, &model.Attachment{}, &model.WidgetFileUpload{})) var inbox model.Inbox var contact model.Contact var contactInbox model.ContactInbox require.NoError(t, db.First(&inbox).Error) require.NoError(t, db.First(&contact).Error) require.NoError(t, db.First(&contactInbox).Error) conversation := &model.Conversation{AccountID: 1, InboxID: inbox.ID, ContactID: contact.ID, ContactInboxID: &contactInbox.ID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"} require.NoError(t, db.Create(conversation).Error) message := &model.Message{AccountID: 1, InboxID: inbox.ID, ConversationID: conversation.ID, MessageType: "incoming", Content: "file"} require.NoError(t, db.Create(message).Error) fileURL := "/uploads/account/1/private.png" require.NoError(t, db.Create(&model.Attachment{AccountID: 1, MessageID: message.ID, FileType: "image", FileURL: fileURL, FileName: "private.png"}).Error) require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "account", "1"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "account", "1", "private.png"), testPNG, 0o600)) svc.WithAccessDB(db) _, ok := svc.ResolveAuthorizedUpload(context.Background(), fileURL, 2, "") assert.False(t, ok) _, ok = svc.ResolveAuthorizedUpload(context.Background(), fileURL, 1, "") assert.True(t, ok) _, ok = svc.ResolveAuthorizedUpload(context.Background(), fileURL, 0, "test-widget") assert.True(t, ok) _, ok = svc.ResolveAuthorizedUpload(context.Background(), fileURL, 0, "wrong-widget") assert.False(t, ok) directURL := "/uploads/widget_direct/direct.png" directMetadata, err := json.Marshal(map[string]any{"contact_inbox_id": contactInbox.ID}) require.NoError(t, err) require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "widget_direct"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "widget_direct", "direct.png"), testPNG, 0o600)) require.NoError(t, db.Create(&model.DirectUpload{ UploadUUID: "private-direct", AccountID: 1, Status: model.DirectUploadStatusPending, Source: model.DirectUploadSourceWidget, OriginalName: "direct.png", FileType: "image", MimeType: "image/png", FileSize: int64(len(testPNG)), FileURL: directURL, Metadata: directMetadata, ExpiresAt: time.Now().Add(time.Hour), }).Error) _, ok = svc.ResolveAuthorizedUpload(context.Background(), directURL, 0, "test-widget") assert.True(t, ok) _, ok = svc.ResolveAuthorizedUpload(context.Background(), directURL, 2, "") assert.False(t, ok) widgetURL := "/uploads/widget/1/session.png" require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "widget", "1"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "widget", "1", "session.png"), testPNG, 0o600)) require.NoError(t, db.Create(&model.WidgetFileUpload{ UploadUUID: "private-widget", WidgetToken: "test-widget", InboxID: inbox.ID, ContactID: contact.ID, Status: model.WidgetFileUploadStatusPending, OriginalName: "session.png", FileType: "image", MimeType: "image/png", FileSize: int64(len(testPNG)), FileURL: widgetURL, ExpiresAt: time.Now().Add(time.Hour), }).Error) _, ok = svc.ResolveAuthorizedUpload(context.Background(), widgetURL, 0, "test-widget") assert.True(t, ok) _, ok = svc.ResolveAuthorizedUpload(context.Background(), widgetURL, 0, "wrong-widget") assert.False(t, ok) otherInbox := &model.Inbox{AccountID: 1, Name: "Other Widget Inbox", ChannelType: "web_widget", ChannelConfig: `{"website_token":"other-website"}`, Enabled: true} require.NoError(t, db.Create(otherInbox).Error) require.NoError(t, db.Create(&model.ContactInbox{ContactID: contact.ID, InboxID: otherInbox.ID, PubsubToken: "other-widget"}).Error) _, ok = svc.ResolveAuthorizedUpload(context.Background(), widgetURL, 0, "other-widget") assert.False(t, ok) avatarURL := "/uploads/account/1/avatar.png" require.NoError(t, os.WriteFile(filepath.Join(tmpDir, "account", "1", "avatar.png"), testPNG, 0o600)) _, ok = svc.ResolveAuthorizedUpload(context.Background(), avatarURL, 1, "") assert.True(t, ok) _, ok = svc.ResolveAuthorizedUpload(context.Background(), avatarURL, 2, "") assert.False(t, ok) } func TestUploadService_CleanupExpiredUploads(t *testing.T) { db, svc, tmpDir := setupUploadServiceTest(t) // Insert a pending-but-expired upload record (pending status with past expires_at) // BatchDeleteExpired deletes pending uploads where expires_at < now expiredUpload := &model.DirectUpload{ UploadUUID: "expired-uuid-123", AccountID: 1, Status: model.DirectUploadStatusPending, Source: model.DirectUploadSourceAccount, OriginalName: "old_file.png", FileType: "image", MimeType: "image/png", FileSize: 100, FileURL: "/uploads/account/1/old_file.png", ExpiresAt: time.Now().Add(-1 * time.Hour), // already past expiry } require.NoError(t, db.Create(expiredUpload).Error) expiredPath := filepath.Join(tmpDir, "account", "1", "old_file.png") require.NoError(t, os.MkdirAll(filepath.Dir(expiredPath), 0o755)) require.NoError(t, os.WriteFile(expiredPath, []byte("expired"), 0o600)) // Insert a non-expired upload (pending status with future expires_at) activeUpload := &model.DirectUpload{ UploadUUID: "active-uuid-456", AccountID: 1, Status: model.DirectUploadStatusPending, Source: model.DirectUploadSourceAccount, OriginalName: "new_file.png", FileType: "image", MimeType: "image/png", FileSize: 200, FileURL: "/uploads/account/1/new_file.png", ExpiresAt: time.Now().Add(24 * time.Hour), } require.NoError(t, db.Create(activeUpload).Error) failedUpload := &model.DirectUpload{ UploadUUID: "failed-uuid-789", AccountID: 1, Status: model.DirectUploadStatusPending, Source: model.DirectUploadSourceAccount, OriginalName: "invalid.png", FileType: "image", MimeType: "image/png", FileSize: 100, FileURL: "https://example.test/invalid.png", ExpiresAt: time.Now().Add(-time.Hour), } require.NoError(t, db.Create(failedUpload).Error) count, err := svc.CleanupExpiredUploads(context.Background()) require.ErrorContains(t, err, "invalid upload path") assert.Equal(t, int64(1), count, "should clean up 1 expired upload") assert.NoFileExists(t, expiredPath) // Verify active upload still exists var remaining model.DirectUpload require.NoError(t, db.Where("upload_uuid = ?", "active-uuid-456").First(&remaining).Error) assert.Equal(t, model.DirectUploadStatusPending, remaining.Status) remaining = model.DirectUpload{} require.NoError(t, db.Where("upload_uuid = ?", "failed-uuid-789").First(&remaining).Error) } func TestUploadService_ReconcilesCrashWindowOrphans(t *testing.T) { db, svc, storagePath := setupUploadServiceTest(t) dir := filepath.Join(storagePath, "account", "1") require.NoError(t, os.MkdirAll(dir, 0o755)) orphan := filepath.Join(dir, "crash-window.png") referenced := filepath.Join(dir, "referenced.png") fresh := filepath.Join(dir, "in-flight.png") for _, path := range []string{orphan, referenced, fresh} { require.NoError(t, os.WriteFile(path, testPNG, 0o600)) } old := time.Now().Add(-uploadCleanupInterval - time.Hour) require.NoError(t, os.Chtimes(orphan, old, old)) require.NoError(t, os.Chtimes(referenced, old, old)) require.NoError(t, db.Create(&model.DirectUpload{ UploadUUID: "referenced", AccountID: 1, Status: model.DirectUploadStatusPending, Source: model.DirectUploadSourceAccount, OriginalName: "referenced.png", FileType: "image", MimeType: "image/png", FileSize: int64(len(testPNG)), FileURL: "/uploads/account/1/referenced.png", ExpiresAt: time.Now().Add(time.Hour), }).Error) count, err := svc.CleanupExpiredUploads(context.Background()) require.NoError(t, err) assert.Equal(t, int64(1), count) assert.NoFileExists(t, orphan) assert.FileExists(t, referenced) assert.FileExists(t, fresh) count, err = svc.CleanupExpiredUploads(context.Background()) require.NoError(t, err) assert.Zero(t, count) }