451 lines
8.2 KiB
Go
451 lines
8.2 KiB
Go
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")
|
|
}
|
|
}
|