package filemanagerrealtime import ( "context" "fmt" "strings" "testing" "time" "gorm.io/driver/sqlite" "gorm.io/gorm" filemanager "wucher/internal/domain/file_manager" "wucher/internal/shared/pkg/uuidv7" ) func openStatusPollerTestDB(t *testing.T) *gorm.DB { t.Helper() dsn := fmt.Sprintf("file:file_status_poller_%d?mode=memory&cache=shared", time.Now().UnixNano()) db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{ NowFunc: func() time.Time { return time.Now().UTC() }, }) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate(&filemanager.FileStatusRealtimeEvent{}); err != nil { t.Fatalf("auto migrate: %v", err) } if err := db.AutoMigrate(&filemanager.File{}); err != nil { t.Fatalf("auto migrate file: %v", err) } return db } func waitStatusEvent(t *testing.T, sub *Subscription) FileStatusEvent { t.Helper() select { case event := <-sub.Events(): return event case <-time.After(time.Second): t.Fatalf("timed out waiting for status event") } return FileStatusEvent{} } func insertRealtimeEventForPollerTest( t *testing.T, db *gorm.DB, fileID []byte, userID []byte, status string, name string, reason string, occurredAt time.Time, ) { t.Helper() row := &filemanager.FileStatusRealtimeEvent{ ID: uuidv7.MustBytes(), FileID: append([]byte(nil), fileID...), UserID: append([]byte(nil), userID...), Status: status, Name: name, FailureReason: reason, OccurredAt: occurredAt.UTC(), CreatedAt: occurredAt.UTC(), UpdatedAt: occurredAt.UTC(), } if err := db.WithContext(context.Background()).Create(row).Error; err != nil { t.Fatalf("insert realtime event: %v", err) } } func insertFileForPollerTest(t *testing.T, db *gorm.DB, fileID []byte, folderID []byte, mimeType string, name string) { t.Helper() now := time.Now().UTC() row := &filemanager.File{ ID: append([]byte(nil), fileID...), FolderID: append([]byte(nil), folderID...), Name: name, NameNormalized: strings.ToLower(strings.TrimSpace(name)), Extension: "pdf", SizeBytes: 1, MimeType: mimeType, Bucket: "bucket", ObjectKey: "obj-" + name, Status: filemanager.FileStatusUploaded, NameSlot: "live", CreatedAt: now, UpdatedAt: now, } if err := db.WithContext(context.Background()).Create(row).Error; err != nil { t.Fatalf("insert file: %v", err) } } func TestStatusPollerEmitsLifecycleEvents(t *testing.T) { db := openStatusPollerTestDB(t) fileID := uuidv7.MustBytes() fileUUID, err := uuidv7.BytesToString(fileID) if err != nil { t.Fatalf("file uuid: %v", err) } ownerID := uuidv7.MustBytes() ownerUUID, err := uuidv7.BytesToString(ownerID) if err != nil { t.Fatalf("owner uuid: %v", err) } folderID := uuidv7.MustBytes() folderUUID, err := uuidv7.BytesToString(folderID) if err != nil { t.Fatalf("folder uuid: %v", err) } insertFileForPollerTest(t, db, fileID, folderID, "application/pdf", "report.pdf") hub := NewHub(8, nil) sub, err := hub.Subscribe(ownerUUID) if err != nil { t.Fatalf("subscribe: %v", err) } defer sub.Close() poller := NewStatusPoller(db, hub, nil, StatusPollerConfig{ PollInterval: time.Second, BatchSize: 50, }) baseAt := time.Now().UTC().Add(20 * time.Millisecond) insertRealtimeEventForPollerTest(t, db, fileID, ownerID, filemanager.FileStatusUploaded, "report.pdf", "", baseAt) insertRealtimeEventForPollerTest(t, db, fileID, ownerID, filemanager.FileStatusProcessing, "report.pdf", "", baseAt.Add(time.Second)) insertRealtimeEventForPollerTest(t, db, fileID, ownerID, filemanager.FileStatusValidated, "report.pdf", "", baseAt.Add(2*time.Second)) insertRealtimeEventForPollerTest(t, db, fileID, ownerID, filemanager.FileStatusReady, "report.pdf", "", baseAt.Add(3*time.Second)) processed, err := poller.PollOnce(context.Background()) if err != nil { t.Fatalf("poll once: %v", err) } if processed != 4 { t.Fatalf("expected 4 published events, got %d", processed) } event := waitStatusEvent(t, sub) if event.Status != filemanager.FileStatusUploaded { t.Fatalf("expected uploaded status, got %s", event.Status) } if event.FileUUID != fileUUID { t.Fatalf("expected file uuid %s, got %s", fileUUID, event.FileUUID) } if event.FolderUUID == nil || !strings.EqualFold(*event.FolderUUID, folderUUID) { t.Fatalf("expected folder uuid %s, got %#v", folderUUID, event.FolderUUID) } if event.MimeType != "application/pdf" { t.Fatalf("expected mime type application/pdf, got %q", event.MimeType) } event = waitStatusEvent(t, sub) if event.Status != filemanager.FileStatusProcessing { t.Fatalf("expected processing status, got %s", event.Status) } event = waitStatusEvent(t, sub) if event.Status != filemanager.FileStatusValidated { t.Fatalf("expected validated status, got %s", event.Status) } event = waitStatusEvent(t, sub) if event.Status != filemanager.FileStatusReady { t.Fatalf("expected ready status, got %s", event.Status) } var deliveredCount int64 if err := db.WithContext(context.Background()). Model(&filemanager.FileStatusRealtimeEvent{}). Where("delivered_at IS NOT NULL"). Count(&deliveredCount).Error; err != nil { t.Fatalf("count delivered rows: %v", err) } if deliveredCount != 4 { t.Fatalf("expected 4 delivered rows, got %d", deliveredCount) } } func TestStatusPollerInvokesOnPublishedCallback(t *testing.T) { db := openStatusPollerTestDB(t) fileID := uuidv7.MustBytes() fileUUID, err := uuidv7.BytesToString(fileID) if err != nil { t.Fatalf("file uuid: %v", err) } userID := uuidv7.MustBytes() userUUID, err := uuidv7.BytesToString(userID) if err != nil { t.Fatalf("user uuid: %v", err) } folderID := uuidv7.MustBytes() insertFileForPollerTest(t, db, fileID, folderID, "application/pdf", "report.pdf") insertRealtimeEventForPollerTest(t, db, fileID, userID, filemanager.FileStatusReady, "report.pdf", "", time.Now().UTC().Add(20*time.Millisecond)) hub := NewHub(4, nil) sub, err := hub.Subscribe(userUUID) if err != nil { t.Fatalf("subscribe: %v", err) } defer sub.Close() var callbackCount int var callbackEvent FileStatusEvent poller := NewStatusPoller(db, hub, nil, StatusPollerConfig{ PollInterval: time.Second, BatchSize: 50, OnPublished: func(_ context.Context, event FileStatusEvent) error { callbackCount++ callbackEvent = event return nil }, }) processed, err := poller.PollOnce(context.Background()) if err != nil { t.Fatalf("poll once failed: %v", err) } if processed != 1 { t.Fatalf("expected one published event, got %d", processed) } if callbackCount != 1 { t.Fatalf("expected callback to run once, got %d", callbackCount) } if callbackEvent.Status != filemanager.FileStatusReady { t.Fatalf("expected ready callback event, got %s", callbackEvent.Status) } if callbackEvent.FileUUID != fileUUID { t.Fatalf("expected callback file uuid %s, got %s", fileUUID, callbackEvent.FileUUID) } _ = waitStatusEvent(t, sub) } func TestStatusPollerEmitsFailedReason(t *testing.T) { db := openStatusPollerTestDB(t) fileID := uuidv7.MustBytes() ownerID := uuidv7.MustBytes() ownerUUID, err := uuidv7.BytesToString(ownerID) if err != nil { t.Fatalf("owner uuid: %v", err) } hub := NewHub(4, nil) sub, err := hub.Subscribe(ownerUUID) if err != nil { t.Fatalf("subscribe: %v", err) } defer sub.Close() poller := NewStatusPoller(db, hub, nil, StatusPollerConfig{ PollInterval: time.Second, BatchSize: 50, }) insertRealtimeEventForPollerTest( t, db, fileID, ownerID, filemanager.FileStatusFailed, "invalid.pdf", "file bucket mismatch", time.Now().UTC().Add(20*time.Millisecond), ) processed, err := poller.PollOnce(context.Background()) if err != nil { t.Fatalf("poll once failed: %v", err) } if processed != 1 { t.Fatalf("expected one processed failed row, got %d", processed) } event := waitStatusEvent(t, sub) if event.Status != filemanager.FileStatusFailed { t.Fatalf("expected failed status event, got %s", event.Status) } if event.FailureReason == nil || *event.FailureReason == "" { t.Fatalf("expected failure reason on failed event") } } func TestStatusPollerSkipsRowsWithoutValidRecipient(t *testing.T) { db := openStatusPollerTestDB(t) fileID := uuidv7.MustBytes() invalidUserID := []byte{1, 2, 3} insertRealtimeEventForPollerTest( t, db, fileID, invalidUserID, filemanager.FileStatusUploaded, "nobody.pdf", "", time.Now().UTC().Add(20*time.Millisecond), ) hub := NewHub(4, nil) poller := NewStatusPoller(db, hub, nil, StatusPollerConfig{ PollInterval: time.Second, BatchSize: 50, }) processed, err := poller.PollOnce(context.Background()) if err != nil { t.Fatalf("poll once: %v", err) } if processed != 0 { t.Fatalf("expected zero emitted events for row without valid recipient, got %d", processed) } var deliveredCount int64 if err := db.WithContext(context.Background()). Model(&filemanager.FileStatusRealtimeEvent{}). Where("delivered_at IS NOT NULL"). Count(&deliveredCount).Error; err != nil { t.Fatalf("count delivered rows: %v", err) } if deliveredCount != 1 { t.Fatalf("expected skipped row to be marked delivered, got %d", deliveredCount) } }