feat(store): add blob, file, folder, and upload stores
Add BlobStore (ref counting), FileStore (soft delete + pagination), FolderStore (materialized path), UploadStore (idempotent upsert), and update AutoMigrate. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
146
internal/store/blob_store_test.go
Normal file
146
internal/store/blob_store_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gcy_hpc_server/internal/model"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func setupBlobTestDB(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.FileBlob{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestBlobStore_Create(t *testing.T) {
|
||||
db := setupBlobTestDB(t)
|
||||
store := NewBlobStore(db)
|
||||
ctx := context.Background()
|
||||
|
||||
blob := &model.FileBlob{
|
||||
SHA256: "abc123",
|
||||
MinioKey: "files/abc123",
|
||||
FileSize: 1024,
|
||||
MimeType: "application/octet-stream",
|
||||
RefCount: 0,
|
||||
}
|
||||
if err := store.Create(ctx, blob); err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
if blob.ID == 0 {
|
||||
t.Error("Create() did not set ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobStore_GetBySHA256(t *testing.T) {
|
||||
db := setupBlobTestDB(t)
|
||||
store := NewBlobStore(db)
|
||||
ctx := context.Background()
|
||||
|
||||
store.Create(ctx, &model.FileBlob{SHA256: "abc", MinioKey: "files/abc", FileSize: 100, RefCount: 0})
|
||||
|
||||
blob, err := store.GetBySHA256(ctx, "abc")
|
||||
if err != nil {
|
||||
t.Fatalf("GetBySHA256() error = %v", err)
|
||||
}
|
||||
if blob == nil {
|
||||
t.Fatal("GetBySHA256() returned nil")
|
||||
}
|
||||
if blob.SHA256 != "abc" {
|
||||
t.Errorf("SHA256 = %q, want %q", blob.SHA256, "abc")
|
||||
}
|
||||
if blob.RefCount != 0 {
|
||||
t.Errorf("RefCount = %d, want 0", blob.RefCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobStore_GetBySHA256_NotFound(t *testing.T) {
|
||||
db := setupBlobTestDB(t)
|
||||
store := NewBlobStore(db)
|
||||
ctx := context.Background()
|
||||
|
||||
blob, err := store.GetBySHA256(ctx, "nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("GetBySHA256() error = %v", err)
|
||||
}
|
||||
if blob != nil {
|
||||
t.Error("GetBySHA256() should return nil for not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobStore_IncrementDecrementRef(t *testing.T) {
|
||||
db := setupBlobTestDB(t)
|
||||
store := NewBlobStore(db)
|
||||
ctx := context.Background()
|
||||
|
||||
store.Create(ctx, &model.FileBlob{SHA256: "abc", MinioKey: "files/abc", FileSize: 100, RefCount: 0})
|
||||
|
||||
if err := store.IncrementRef(ctx, "abc"); err != nil {
|
||||
t.Fatalf("IncrementRef() error = %v", err)
|
||||
}
|
||||
blob, _ := store.GetBySHA256(ctx, "abc")
|
||||
if blob.RefCount != 1 {
|
||||
t.Errorf("RefCount after 1st increment = %d, want 1", blob.RefCount)
|
||||
}
|
||||
|
||||
store.IncrementRef(ctx, "abc")
|
||||
blob, _ = store.GetBySHA256(ctx, "abc")
|
||||
if blob.RefCount != 2 {
|
||||
t.Errorf("RefCount after 2nd increment = %d, want 2", blob.RefCount)
|
||||
}
|
||||
|
||||
refCount, err := store.DecrementRef(ctx, "abc")
|
||||
if err != nil {
|
||||
t.Fatalf("DecrementRef() error = %v", err)
|
||||
}
|
||||
if refCount != 1 {
|
||||
t.Errorf("DecrementRef() returned %d, want 1", refCount)
|
||||
}
|
||||
|
||||
refCount, _ = store.DecrementRef(ctx, "abc")
|
||||
if refCount != 0 {
|
||||
t.Errorf("DecrementRef() returned %d, want 0", refCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobStore_Delete(t *testing.T) {
|
||||
db := setupBlobTestDB(t)
|
||||
store := NewBlobStore(db)
|
||||
ctx := context.Background()
|
||||
|
||||
store.Create(ctx, &model.FileBlob{SHA256: "abc", MinioKey: "files/abc", FileSize: 100, RefCount: 0})
|
||||
|
||||
if err := store.Delete(ctx, "abc"); err != nil {
|
||||
t.Fatalf("Delete() error = %v", err)
|
||||
}
|
||||
|
||||
blob, _ := store.GetBySHA256(ctx, "abc")
|
||||
if blob != nil {
|
||||
t.Error("Delete() did not remove blob")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBlobStore_SHA256_UniqueConstraint(t *testing.T) {
|
||||
db := setupBlobTestDB(t)
|
||||
store := NewBlobStore(db)
|
||||
ctx := context.Background()
|
||||
|
||||
store.Create(ctx, &model.FileBlob{SHA256: "dup", MinioKey: "files/dup1", FileSize: 100})
|
||||
err := store.Create(ctx, &model.FileBlob{SHA256: "dup", MinioKey: "files/dup2", FileSize: 200})
|
||||
if err == nil {
|
||||
t.Error("expected error for duplicate SHA256, got nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user