package pfs_db import ( "fmt" "testing" "time" "github.com/google/uuid" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // // ==================== TEST SETUP ==================== // func newTestDB(t *testing.T) *PFSDB { t.Helper() db, err := gorm.Open( sqlite.Open("file::memory:?cache=shared&_foreign_keys=on"), &gorm.Config{}, ) if err != nil { t.Fatalf("failed to open sqlite: %v", err) } sqlDB, err := db.DB() if err != nil { t.Fatalf("failed to get sql db: %v", err) } // 🔥 КРИТИЧНО для in-memory SQLite sqlDB.SetMaxOpenConns(1) if _, err := sqlDB.Exec("PRAGMA foreign_keys = ON"); err != nil { t.Fatalf("failed to enable foreign keys: %v", err) } pfs := &PFSDB{DB: db} if err := pfs.Migrate(); err != nil { t.Fatalf("migration failed: %v", err) } return pfs } // // ==================== HELPERS ==================== // func countDirs(t *testing.T, db *gorm.DB) int64 { t.Helper() var c int64 if err := db.Model(&DirModel{}).Count(&c).Error; err != nil { t.Fatal(err) } return c } func countFiles(t *testing.T, db *gorm.DB) int64 { t.Helper() var c int64 if err := db.Model(&FileModel{}).Count(&c).Error; err != nil { t.Fatal(err) } return c } func countFileData(t *testing.T, db *gorm.DB) int64 { t.Helper() var c int64 if err := db.Model(&FileDataModel{}).Count(&c).Error; err != nil { t.Fatal(err) } return c } // // ==================== TEST: DIR CASCADE ==================== // func TestCascade_DeleteDirectoryTree(t *testing.T) { pfs := newTestDB(t) db := pfs.DB userID := uuid.New() // root root := DirModel{ ID: uuid.New(), Name: "root", OwnerID: userID, } if err := db.Create(&root).Error; err != nil { t.Fatal(err) } // child child := DirModel{ ID: uuid.New(), ParentID: &root.ID, Name: "child", OwnerID: userID, } if err := db.Create(&child).Error; err != nil { t.Fatal(err) } // grandchild grandchild := DirModel{ ID: uuid.New(), ParentID: &child.ID, Name: "grandchild", OwnerID: userID, } if err := db.Create(&grandchild).Error; err != nil { t.Fatal(err) } // file inside grandchild file := FileModel{ ID: uuid.New(), DirID: &grandchild.ID, Name: "file.txt", OwnerID: userID, } if err := db.Create(&file).Error; err != nil { t.Fatal(err) } fileData := FileDataModel{ ID: uuid.New(), FileID: file.ID, Data: []byte("hello"), } if err := db.Create(&fileData).Error; err != nil { t.Fatal(err) } // sanity check if countDirs(t, db) != 3 { t.Fatal("expected 3 dirs") } if countFiles(t, db) != 1 { t.Fatal("expected 1 file") } if countFileData(t, db) != 1 { t.Fatal("expected 1 file data") } // DELETE ROOT → should cascade everything if err := db.Delete(&root).Error; err != nil { t.Fatal(err) } if countDirs(t, db) != 0 { t.Fatal("dirs were not cascade deleted") } if countFiles(t, db) != 0 { t.Fatal("files were not cascade deleted") } if countFileData(t, db) != 0 { t.Fatal("file data was not cascade deleted") } } // // ==================== TEST: FILE CASCADE ==================== // func TestCascade_DeleteFileRemovesData(t *testing.T) { pfs := newTestDB(t) db := pfs.DB userID := uuid.New() file := FileModel{ ID: uuid.New(), Name: "test.txt", OwnerID: userID, } if err := db.Create(&file).Error; err != nil { t.Fatal(err) } fileData := FileDataModel{ ID: uuid.New(), FileID: file.ID, Data: []byte("payload"), } if err := db.Create(&fileData).Error; err != nil { t.Fatal(err) } if countFiles(t, db) != 1 || countFileData(t, db) != 1 { t.Fatal("setup failed") } // delete file if err := db.Delete(&file).Error; err != nil { t.Fatal(err) } if countFiles(t, db) != 0 { t.Fatal("file not deleted") } if countFileData(t, db) != 0 { t.Fatal("file data not cascade deleted") } } // // ==================== TEST: FK ENFORCEMENT ==================== // func TestForeignKey_RejectOrphanFileData(t *testing.T) { pfs := newTestDB(t) db := pfs.DB orphan := FileDataModel{ ID: uuid.New(), FileID: uuid.New(), // несуществующий файл Data: []byte("bad"), } err := db.Create(&orphan).Error if err == nil { t.Fatal("expected FK violation, got nil") } } func TestStress_CascadeDeleteLargeTree(t *testing.T) { pfs := newTestDB(t) db := pfs.DB userID := uuid.New() const ( dirCount = 200 filesPerDir = 20 ) err := db.Transaction(func(tx *gorm.DB) error { // --- root --- root := DirModel{ ID: uuid.New(), Name: "root", OwnerID: userID, } if err := tx.Create(&root).Error; err != nil { return err } // --- цепочка директорий --- parentID := root.ID var dirs []DirModel for i := 0; i < dirCount; i++ { dir := DirModel{ ID: uuid.New(), ParentID: &parentID, Name: "dir", OwnerID: userID, } if err := tx.Create(&dir).Error; err != nil { return fmt.Errorf("failed to insert dir %d: %w", i, err) } dirs = append(dirs, dir) parentID = dir.ID } // --- файлы --- for _, dir := range dirs { for i := 0; i < filesPerDir; i++ { fileID := uuid.New() file := FileModel{ ID: fileID, DirID: &dir.ID, Name: "file", OwnerID: userID, } if err := tx.Create(&file).Error; err != nil { return err } data := FileDataModel{ ID: uuid.New(), FileID: fileID, Data: []byte("payload"), } if err := tx.Create(&data).Error; err != nil { return err } } } t.Logf("Created: %d dirs, %d files", dirCount+1, dirCount*filesPerDir) // --- удаление --- start := time.Now() if err := tx.Delete(&root).Error; err != nil { return err } elapsed := time.Since(start) // --- проверки --- var count int64 if err := tx.Model(&DirModel{}).Count(&count).Error; err != nil { return err } if count != 0 { return fmt.Errorf("dirs not fully deleted: %d", count) } if err := tx.Model(&FileModel{}).Count(&count).Error; err != nil { return err } if count != 0 { return fmt.Errorf("files not fully deleted: %d", count) } if err := tx.Model(&FileDataModel{}).Count(&count).Error; err != nil { return err } if count != 0 { return fmt.Errorf("file data not fully deleted: %d", count) } t.Logf("Cascade delete took: %v", elapsed) return nil }) if err != nil { t.Fatal(err) } } func mustCreate(t *testing.T, db *gorm.DB, v any) { t.Helper() if err := db.Create(v).Error; err != nil { t.Fatal(err) } } func TestResolvePath(t *testing.T) { pfs := newTestDB(t) db := pfs.DB userID := uuid.New() // --- структура: // /root/a/b/c // /root/a/b/file.txt root := DirModel{ ID: uuid.New(), Name: "root", OwnerID: userID, } mustCreate(t, db, &root) a := DirModel{ ID: uuid.New(), Name: "a", ParentID: &root.ID, OwnerID: userID, } mustCreate(t, db, &a) b := DirModel{ ID: uuid.New(), Name: "b", ParentID: &a.ID, OwnerID: userID, } mustCreate(t, db, &b) c := DirModel{ ID: uuid.New(), Name: "c", ParentID: &b.ID, OwnerID: userID, } mustCreate(t, db, &c) file := FileModel{ ID: uuid.New(), Name: "file.txt", DirID: &b.ID, OwnerID: userID, } mustCreate(t, db, &file) data := FileDataModel{ ID: uuid.New(), FileID: file.ID, Data: []byte("hello"), } mustCreate(t, db, &data) // --- ResolveDir success --- dirID, err := pfs.ResolveDirByPath("/root/a/b/c", userID) if err != nil { t.Fatal(err) } if dirID != c.ID { t.Fatalf("expected %s got %s", c.ID, dirID) } // --- ResolveFile success --- fileID, err := pfs.ResolveFileByPath("/root/a/b/file.txt", userID) if err != nil { t.Fatal(err) } if fileID != file.ID { t.Fatalf("expected %s got %s", file.ID, fileID) } // --- missing dir --- _, err = pfs.ResolveDirByPath("/root/a/x", userID) if err == nil { t.Fatal("expected error for missing dir") } // --- missing file --- _, err = pfs.ResolveFileByPath("/root/a/b/missing.txt", userID) if err == nil { t.Fatal("expected error for missing file") } // --- другой пользователь --- otherUser := uuid.New() _, err = pfs.ResolveDirByPath("/root/a/b/c", otherUser) if err == nil { t.Fatal("expected access error for other user") } }