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:
294
internal/store/folder_store_test.go
Normal file
294
internal/store/folder_store_test.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gcy_hpc_server/internal/model"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
func setupFolderTestDB(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.Folder{}, &model.File{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestFolderStore_CreateAndGetByID(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
folder := &model.Folder{
|
||||
Name: "data",
|
||||
Path: "/data/",
|
||||
}
|
||||
if err := s.Create(context.Background(), folder); err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
if folder.ID <= 0 {
|
||||
t.Fatalf("Create() id = %d, want positive", folder.ID)
|
||||
}
|
||||
|
||||
got, err := s.GetByID(context.Background(), folder.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetByID() error = %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("GetByID() returned nil")
|
||||
}
|
||||
if got.Name != "data" {
|
||||
t.Errorf("Name = %q, want %q", got.Name, "data")
|
||||
}
|
||||
if got.Path != "/data/" {
|
||||
t.Errorf("Path = %q, want %q", got.Path, "/data/")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStore_GetByID_NotFound(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
got, err := s.GetByID(context.Background(), 99999)
|
||||
if err != nil {
|
||||
t.Fatalf("GetByID() error = %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Error("GetByID() expected nil for not-found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStore_GetByPath(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
folder := &model.Folder{
|
||||
Name: "data",
|
||||
Path: "/data/",
|
||||
}
|
||||
if err := s.Create(context.Background(), folder); err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
|
||||
got, err := s.GetByPath(context.Background(), "/data/")
|
||||
if err != nil {
|
||||
t.Fatalf("GetByPath() error = %v", err)
|
||||
}
|
||||
if got == nil {
|
||||
t.Fatal("GetByPath() returned nil")
|
||||
}
|
||||
if got.ID != folder.ID {
|
||||
t.Errorf("ID = %d, want %d", got.ID, folder.ID)
|
||||
}
|
||||
|
||||
got, err = s.GetByPath(context.Background(), "/nonexistent/")
|
||||
if err != nil {
|
||||
t.Fatalf("GetByPath() nonexistent error = %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Error("GetByPath() expected nil for nonexistent path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStore_ListByParentID(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
root1 := &model.Folder{Name: "alpha", Path: "/alpha/"}
|
||||
root2 := &model.Folder{Name: "beta", Path: "/beta/"}
|
||||
if err := s.Create(context.Background(), root1); err != nil {
|
||||
t.Fatalf("Create root1: %v", err)
|
||||
}
|
||||
if err := s.Create(context.Background(), root2); err != nil {
|
||||
t.Fatalf("Create root2: %v", err)
|
||||
}
|
||||
|
||||
sub := &model.Folder{Name: "sub", Path: "/alpha/sub/", ParentID: &root1.ID}
|
||||
if err := s.Create(context.Background(), sub); err != nil {
|
||||
t.Fatalf("Create sub: %v", err)
|
||||
}
|
||||
|
||||
roots, err := s.ListByParentID(context.Background(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ListByParentID(nil) error = %v", err)
|
||||
}
|
||||
if len(roots) != 2 {
|
||||
t.Fatalf("root folders = %d, want 2", len(roots))
|
||||
}
|
||||
if roots[0].Name != "alpha" {
|
||||
t.Errorf("roots[0].Name = %q, want %q (alphabetical)", roots[0].Name, "alpha")
|
||||
}
|
||||
|
||||
children, err := s.ListByParentID(context.Background(), &root1.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ListByParentID(root1) error = %v", err)
|
||||
}
|
||||
if len(children) != 1 {
|
||||
t.Fatalf("children = %d, want 1", len(children))
|
||||
}
|
||||
if children[0].Name != "sub" {
|
||||
t.Errorf("children[0].Name = %q, want %q", children[0].Name, "sub")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStore_GetSubTree(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
data := &model.Folder{Name: "data", Path: "/data/"}
|
||||
if err := s.Create(context.Background(), data); err != nil {
|
||||
t.Fatalf("Create data: %v", err)
|
||||
}
|
||||
results := &model.Folder{Name: "results", Path: "/data/results/", ParentID: &data.ID}
|
||||
if err := s.Create(context.Background(), results); err != nil {
|
||||
t.Fatalf("Create results: %v", err)
|
||||
}
|
||||
other := &model.Folder{Name: "other", Path: "/other/"}
|
||||
if err := s.Create(context.Background(), other); err != nil {
|
||||
t.Fatalf("Create other: %v", err)
|
||||
}
|
||||
|
||||
subtree, err := s.GetSubTree(context.Background(), "/data/")
|
||||
if err != nil {
|
||||
t.Fatalf("GetSubTree() error = %v", err)
|
||||
}
|
||||
if len(subtree) != 2 {
|
||||
t.Fatalf("subtree = %d, want 2", len(subtree))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStore_HasChildren_WithSubFolders(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
parent := &model.Folder{Name: "parent", Path: "/parent/"}
|
||||
if err := s.Create(context.Background(), parent); err != nil {
|
||||
t.Fatalf("Create parent: %v", err)
|
||||
}
|
||||
child := &model.Folder{Name: "child", Path: "/parent/child/", ParentID: &parent.ID}
|
||||
if err := s.Create(context.Background(), child); err != nil {
|
||||
t.Fatalf("Create child: %v", err)
|
||||
}
|
||||
|
||||
has, err := s.HasChildren(context.Background(), parent.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("HasChildren() error = %v", err)
|
||||
}
|
||||
if !has {
|
||||
t.Error("HasChildren() = false, want true (has sub-folders)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStore_HasChildren_WithFiles(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
folder := &model.Folder{Name: "docs", Path: "/docs/"}
|
||||
if err := s.Create(context.Background(), folder); err != nil {
|
||||
t.Fatalf("Create folder: %v", err)
|
||||
}
|
||||
|
||||
file := &model.File{
|
||||
Name: "readme.txt",
|
||||
FolderID: &folder.ID,
|
||||
BlobSHA256: "abc123",
|
||||
}
|
||||
if err := db.Create(file).Error; err != nil {
|
||||
t.Fatalf("Create file: %v", err)
|
||||
}
|
||||
|
||||
has, err := s.HasChildren(context.Background(), folder.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("HasChildren() error = %v", err)
|
||||
}
|
||||
if !has {
|
||||
t.Error("HasChildren() = false, want true (has files)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStore_HasChildren_Empty(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
folder := &model.Folder{Name: "empty", Path: "/empty/"}
|
||||
if err := s.Create(context.Background(), folder); err != nil {
|
||||
t.Fatalf("Create folder: %v", err)
|
||||
}
|
||||
|
||||
has, err := s.HasChildren(context.Background(), folder.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("HasChildren() error = %v", err)
|
||||
}
|
||||
if has {
|
||||
t.Error("HasChildren() = true, want false (empty folder)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStore_HasChildren_NotFound(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
has, err := s.HasChildren(context.Background(), 99999)
|
||||
if err != nil {
|
||||
t.Fatalf("HasChildren() error = %v", err)
|
||||
}
|
||||
if has {
|
||||
t.Error("HasChildren() = true for nonexistent, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStore_Delete(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
folder := &model.Folder{Name: "temp", Path: "/temp/"}
|
||||
if err := s.Create(context.Background(), folder); err != nil {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
|
||||
if err := s.Delete(context.Background(), folder.ID); err != nil {
|
||||
t.Fatalf("Delete() error = %v", err)
|
||||
}
|
||||
|
||||
got, err := s.GetByID(context.Background(), folder.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetByID() after delete error = %v", err)
|
||||
}
|
||||
if got != nil {
|
||||
t.Error("GetByID() after delete returned non-nil, expected soft-deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStore_Delete_Idempotent(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
if err := s.Delete(context.Background(), 99999); err != nil {
|
||||
t.Fatalf("Delete() non-existent error = %v, want nil (idempotent)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStore_Path_UniqueConstraint(t *testing.T) {
|
||||
db := setupFolderTestDB(t)
|
||||
s := NewFolderStore(db)
|
||||
|
||||
f1 := &model.Folder{Name: "data", Path: "/data/"}
|
||||
if err := s.Create(context.Background(), f1); err != nil {
|
||||
t.Fatalf("Create first: %v", err)
|
||||
}
|
||||
|
||||
f2 := &model.Folder{Name: "data2", Path: "/data/"}
|
||||
if err := s.Create(context.Background(), f2); err == nil {
|
||||
t.Fatal("expected error for duplicate path, got nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user