init push

This commit is contained in:
2026-07-16 22:16:45 +07:00
commit 8b068bdb10
1021 changed files with 332816 additions and 0 deletions

View 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)
}
}