package store import ( "context" "testing" "time" "gcy_hpc_server/internal/model" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) func setupUploadTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("open sqlite: %v", err) } if err := db.AutoMigrate(&model.UploadSession{}, &model.UploadChunk{}); err != nil { t.Fatalf("migrate: %v", err) } return db } func newTestSession(status string, expiresAt time.Time) *model.UploadSession { return &model.UploadSession{ FileName: "test.bin", FileSize: 100 * 1024 * 1024, ChunkSize: 16 << 20, TotalChunks: 7, SHA256: "abc123", Status: status, MinioPrefix: "uploads/1/", ExpiresAt: expiresAt, } } func newTestChunk(sessionID int64, index int, status string) *model.UploadChunk { return &model.UploadChunk{ SessionID: sessionID, ChunkIndex: index, MinioKey: "uploads/1/chunk_00000", SHA256: "chunk_hash_0", Size: 16 << 20, Status: status, } } func TestUploadStore_CreateSession(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) session := newTestSession("pending", time.Now().Add(48*time.Hour)) if err := s.CreateSession(context.Background(), session); err != nil { t.Fatalf("CreateSession() error = %v", err) } if session.ID <= 0 { t.Errorf("ID = %d, want positive", session.ID) } if session.FileName != "test.bin" { t.Errorf("FileName = %q, want %q", session.FileName, "test.bin") } if session.ExpiresAt.IsZero() { t.Error("ExpiresAt is zero, want a real timestamp") } } func TestUploadStore_GetSession(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) session := newTestSession("pending", time.Now().Add(48*time.Hour)) s.CreateSession(context.Background(), session) got, err := s.GetSession(context.Background(), session.ID) if err != nil { t.Fatalf("GetSession() error = %v", err) } if got == nil { t.Fatal("GetSession() returned nil") } if got.FileName != session.FileName { t.Errorf("FileName = %q, want %q", got.FileName, session.FileName) } if got.Status != "pending" { t.Errorf("Status = %q, want %q", got.Status, "pending") } } func TestUploadStore_GetSession_NotFound(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) got, err := s.GetSession(context.Background(), 99999) if err != nil { t.Fatalf("GetSession() error = %v", err) } if got != nil { t.Error("GetSession() expected nil for not-found") } } func TestUploadStore_UpdateSessionStatus(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) session := newTestSession("pending", time.Now().Add(48*time.Hour)) s.CreateSession(context.Background(), session) if err := s.UpdateSessionStatus(context.Background(), session.ID, "uploading"); err != nil { t.Fatalf("UpdateSessionStatus() error = %v", err) } got, _ := s.GetSession(context.Background(), session.ID) if got.Status != "uploading" { t.Errorf("Status = %q, want %q", got.Status, "uploading") } } func TestUploadStore_UpdateSessionStatus_NotFound(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) err := s.UpdateSessionStatus(context.Background(), 99999, "uploading") if err == nil { t.Fatal("UpdateSessionStatus() expected error for not-found, got nil") } } func TestUploadStore_GetSessionWithChunks(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) session := newTestSession("uploading", time.Now().Add(48*time.Hour)) s.CreateSession(context.Background(), session) s.UpsertChunk(context.Background(), &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 0, MinioKey: "uploads/1/chunk_0", SHA256: "h0", Size: 16 << 20, Status: "uploaded", }) s.UpsertChunk(context.Background(), &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 2, MinioKey: "uploads/1/chunk_2", SHA256: "h2", Size: 16 << 20, Status: "uploaded", }) gotSession, chunks, err := s.GetSessionWithChunks(context.Background(), session.ID) if err != nil { t.Fatalf("GetSessionWithChunks() error = %v", err) } if gotSession == nil { t.Fatal("GetSessionWithChunks() session is nil") } if len(chunks) != 2 { t.Fatalf("len(chunks) = %d, want 2", len(chunks)) } if chunks[0].ChunkIndex != 0 { t.Errorf("chunks[0].ChunkIndex = %d, want 0", chunks[0].ChunkIndex) } if chunks[1].ChunkIndex != 2 { t.Errorf("chunks[1].ChunkIndex = %d, want 2", chunks[1].ChunkIndex) } } func TestUploadStore_GetSessionWithChunks_NotFound(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) gotSession, chunks, err := s.GetSessionWithChunks(context.Background(), 99999) if err != nil { t.Fatalf("GetSessionWithChunks() error = %v", err) } if gotSession != nil { t.Error("expected nil session for not-found") } if chunks != nil { t.Error("expected nil chunks for not-found") } } func TestUploadStore_UpsertChunk_Idempotent(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) session := newTestSession("uploading", time.Now().Add(48*time.Hour)) s.CreateSession(context.Background(), session) chunk := &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 0, MinioKey: "uploads/1/chunk_0", SHA256: "hash_v1", Size: 1024, Status: "uploaded", } if err := s.UpsertChunk(context.Background(), chunk); err != nil { t.Fatalf("first UpsertChunk() error = %v", err) } chunk2 := &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 0, MinioKey: "uploads/1/chunk_0", SHA256: "hash_v2", Size: 2048, Status: "uploaded", } if err := s.UpsertChunk(context.Background(), chunk2); err != nil { t.Fatalf("second UpsertChunk() error = %v", err) } indices, _ := s.GetUploadedChunkIndices(context.Background(), session.ID) if len(indices) != 1 { t.Errorf("len(indices) = %d, want 1 (idempotent)", len(indices)) } var got model.UploadChunk db.Where("session_id = ? AND chunk_index = ?", session.ID, 0).First(&got) if got.SHA256 != "hash_v2" { t.Errorf("SHA256 = %q, want %q (updated on conflict)", got.SHA256, "hash_v2") } } func TestUploadStore_GetUploadedChunkIndices(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) session := newTestSession("uploading", time.Now().Add(48*time.Hour)) s.CreateSession(context.Background(), session) s.UpsertChunk(context.Background(), &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 0, MinioKey: "k0", Size: 100, Status: "uploaded", }) s.UpsertChunk(context.Background(), &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 1, MinioKey: "k1", Size: 100, Status: "pending", }) s.UpsertChunk(context.Background(), &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 2, MinioKey: "k2", Size: 100, Status: "uploaded", }) indices, err := s.GetUploadedChunkIndices(context.Background(), session.ID) if err != nil { t.Fatalf("GetUploadedChunkIndices() error = %v", err) } if len(indices) != 2 { t.Fatalf("len(indices) = %d, want 2", len(indices)) } if indices[0] != 0 || indices[1] != 2 { t.Errorf("indices = %v, want [0 2]", indices) } } func TestUploadStore_CountUploadedChunks(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) session := newTestSession("uploading", time.Now().Add(48*time.Hour)) s.CreateSession(context.Background(), session) s.UpsertChunk(context.Background(), &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 0, MinioKey: "k0", Size: 100, Status: "uploaded", }) s.UpsertChunk(context.Background(), &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 1, MinioKey: "k1", Size: 100, Status: "uploaded", }) s.UpsertChunk(context.Background(), &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 2, MinioKey: "k2", Size: 100, Status: "pending", }) count, err := s.CountUploadedChunks(context.Background(), session.ID) if err != nil { t.Fatalf("CountUploadedChunks() error = %v", err) } if count != 2 { t.Errorf("count = %d, want 2", count) } } func TestUploadStore_ListExpiredSessions(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) past := time.Now().Add(-1 * time.Hour) future := time.Now().Add(48 * time.Hour) expired := newTestSession("pending", past) expired.FileName = "expired.bin" s.CreateSession(context.Background(), expired) active := newTestSession("uploading", future) active.FileName = "active.bin" s.CreateSession(context.Background(), active) completed := newTestSession("completed", past) completed.FileName = "completed.bin" s.CreateSession(context.Background(), completed) sessions, err := s.ListExpiredSessions(context.Background()) if err != nil { t.Fatalf("ListExpiredSessions() error = %v", err) } if len(sessions) != 1 { t.Fatalf("len(sessions) = %d, want 1", len(sessions)) } if sessions[0].FileName != "expired.bin" { t.Errorf("FileName = %q, want %q", sessions[0].FileName, "expired.bin") } } func TestUploadStore_DeleteSession(t *testing.T) { db := setupUploadTestDB(t) s := NewUploadStore(db) session := newTestSession("uploading", time.Now().Add(48*time.Hour)) s.CreateSession(context.Background(), session) s.UpsertChunk(context.Background(), &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 0, MinioKey: "k0", Size: 100, Status: "uploaded", }) s.UpsertChunk(context.Background(), &model.UploadChunk{ SessionID: session.ID, ChunkIndex: 1, MinioKey: "k1", Size: 100, Status: "uploaded", }) if err := s.DeleteSession(context.Background(), session.ID); err != nil { t.Fatalf("DeleteSession() error = %v", err) } got, _ := s.GetSession(context.Background(), session.ID) if got != nil { t.Error("session still exists after delete") } var chunkCount int64 db.Model(&model.UploadChunk{}).Where("session_id = ?", session.ID).Count(&chunkCount) if chunkCount != 0 { t.Errorf("chunkCount = %d, want 0 after delete", chunkCount) } }