init push
This commit is contained in:
321
internal/realtime/filemanager/status_poller_test.go
Normal file
321
internal/realtime/filemanager/status_poller_test.go
Normal file
@@ -0,0 +1,321 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user