322 lines
9.1 KiB
Go
322 lines
9.1 KiB
Go
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)
|
|
}
|
|
}
|