verstak/internal/core/watcher/watcher_test.go

271 lines
6.6 KiB
Go

package watcher
import (
"os"
"path/filepath"
"testing"
"time"
"verstak/internal/core/activity"
"verstak/internal/core/files"
"verstak/internal/core/nodes"
"verstak/internal/core/storage"
)
func setupWatcherTest(t *testing.T) (string, *storage.DB, *nodes.Repository, *files.Service, *activity.Service, func()) {
t.Helper()
vaultRoot, err := os.MkdirTemp("", "verstak-watcher-*")
if err != nil {
t.Fatal(err)
}
dbDir := filepath.Join(vaultRoot, ".verstak")
if err := os.MkdirAll(dbDir, 0o750); err != nil {
t.Fatal(err)
}
db, err := storage.Open(filepath.Join(dbDir, "index.db"))
if err != nil {
os.RemoveAll(vaultRoot)
t.Fatalf("open db: %v", err)
}
nodeRepo := nodes.NewRepository(db)
fileSvc := files.NewService(db, vaultRoot, nodeRepo)
activitySvc := activity.NewService(db)
cleanup := func() {
db.Close()
os.RemoveAll(vaultRoot)
}
return vaultRoot, db, nodeRepo, fileSvc, activitySvc, cleanup
}
// tempFileOutsideVault creates a temp file not inside the vault for import.
func tempFileOutsideVault(t *testing.T, content string) string {
t.Helper()
f, err := os.CreateTemp("", "verstak-import-*")
if err != nil {
t.Fatal(err)
}
if _, err := f.WriteString(content); err != nil {
t.Fatal(err)
}
f.Close()
return f.Name()
}
func TestScanner_NoChanges(t *testing.T) {
vaultRoot, _, nodeRepo, fileSvc, activitySvc, cleanup := setupWatcherTest(t)
defer cleanup()
node, err := nodeRepo.Create(nil, nodes.TypeFolder, "test-folder", 0, "", "test-folder")
if err != nil {
t.Fatal(err)
}
dir := filepath.Join(vaultRoot, "test-folder")
if err := os.MkdirAll(dir, 0o750); err != nil {
t.Fatal(err)
}
// Import a file into vault (creates record + copies file).
src := tempFileOutsideVault(t, "hello world")
defer os.Remove(src)
_, err = fileSvc.CopyIntoVault(node.ID, src, "test-folder")
if err != nil {
t.Fatal(err)
}
scanner := NewScanner(vaultRoot, nodeRepo, fileSvc, activitySvc)
result, err := scanner.Run()
if err != nil {
t.Fatal(err)
}
if result.MissingFiles != 0 {
t.Errorf("expected 0 missing, got %d", result.MissingFiles)
}
if result.RestoredFiles != 0 {
t.Errorf("expected 0 restored, got %d", result.RestoredFiles)
}
if result.ModifiedFiles != 0 {
t.Errorf("expected 0 modified, got %d", result.ModifiedFiles)
}
if result.NewFiles != 0 {
t.Errorf("expected 0 new, got %d", result.NewFiles)
}
if result.NodesScanned != 1 {
t.Errorf("expected 1 node scanned, got %d", result.NodesScanned)
}
}
func TestScanner_MissingFile(t *testing.T) {
vaultRoot, _, nodeRepo, fileSvc, activitySvc, cleanup := setupWatcherTest(t)
defer cleanup()
node, err := nodeRepo.Create(nil, nodes.TypeFolder, "test-folder", 0, "", "test-folder")
if err != nil {
t.Fatal(err)
}
dir := filepath.Join(vaultRoot, "test-folder")
if err := os.MkdirAll(dir, 0o750); err != nil {
t.Fatal(err)
}
// Import a file.
src := tempFileOutsideVault(t, "bye")
defer os.Remove(src)
_, err = fileSvc.CopyIntoVault(node.ID, src, "test-folder")
if err != nil {
t.Fatal(err)
}
// Now remove the physical file from vault.
vaultFiles, err := fileSvc.ListByNode(node.ID)
if err != nil || len(vaultFiles) == 0 {
t.Fatal("no vault files found")
}
rec := vaultFiles[0]
absPath := filepath.Join(vaultRoot, rec.Path)
if err := os.Remove(absPath); err != nil {
t.Fatal(err)
}
scanner := NewScanner(vaultRoot, nodeRepo, fileSvc, activitySvc)
result, err := scanner.Run()
if err != nil {
t.Fatal(err)
}
if result.MissingFiles != 1 {
t.Errorf("expected 1 missing, got %d", result.MissingFiles)
}
// Verify the file record is now marked missing.
trashed, err := fileSvc.ListTrashedByNode(node.ID)
if err != nil {
t.Fatal(err)
}
if len(trashed) != 1 {
t.Errorf("expected 1 trashed record, got %d", len(trashed))
}
if !trashed[0].Missing {
t.Error("expected record to be marked missing")
}
}
func TestScanner_RestoredFile(t *testing.T) {
vaultRoot, _, nodeRepo, fileSvc, activitySvc, cleanup := setupWatcherTest(t)
defer cleanup()
node, err := nodeRepo.Create(nil, nodes.TypeFolder, "test-folder", 0, "", "test-folder")
if err != nil {
t.Fatal(err)
}
dir := filepath.Join(vaultRoot, "test-folder")
if err := os.MkdirAll(dir, 0o750); err != nil {
t.Fatal(err)
}
// Import a file.
src := tempFileOutsideVault(t, "back")
defer os.Remove(src)
rec, err := fileSvc.CopyIntoVault(node.ID, src, "test-folder")
if err != nil {
t.Fatal(err)
}
// Mark as missing and remove from disk.
if err := fileSvc.MarkMissing(rec.ID, true); err != nil {
t.Fatal(err)
}
absPath := filepath.Join(vaultRoot, rec.Path)
if err := os.Remove(absPath); err != nil {
t.Fatal(err)
}
// Re-create the file.
if err := os.WriteFile(absPath, []byte("back again"), 0o640); err != nil {
t.Fatal(err)
}
scanner := NewScanner(vaultRoot, nodeRepo, fileSvc, activitySvc)
result, err := scanner.Run()
if err != nil {
t.Fatal(err)
}
if result.RestoredFiles != 1 {
t.Errorf("expected 1 restored, got %d", result.RestoredFiles)
}
}
func TestScanner_ModifiedFile(t *testing.T) {
vaultRoot, _, nodeRepo, fileSvc, activitySvc, cleanup := setupWatcherTest(t)
defer cleanup()
node, err := nodeRepo.Create(nil, nodes.TypeFolder, "test-folder", 0, "", "test-folder")
if err != nil {
t.Fatal(err)
}
dir := filepath.Join(vaultRoot, "test-folder")
if err := os.MkdirAll(dir, 0o750); err != nil {
t.Fatal(err)
}
// Import a file.
src := tempFileOutsideVault(t, "original")
defer os.Remove(src)
_, err = fileSvc.CopyIntoVault(node.ID, src, "test-folder")
if err != nil {
t.Fatal(err)
}
// Get the record to know the vault path.
vaultFiles, err := fileSvc.ListByNode(node.ID)
if err != nil || len(vaultFiles) == 0 {
t.Fatal("no vault files found")
}
rec := vaultFiles[0]
absPath := filepath.Join(vaultRoot, rec.Path)
// Modify the file content.
time.Sleep(10 * time.Millisecond)
if err := os.WriteFile(absPath, []byte("modified"), 0o640); err != nil {
t.Fatal(err)
}
scanner := NewScanner(vaultRoot, nodeRepo, fileSvc, activitySvc)
result, err := scanner.Run()
if err != nil {
t.Fatal(err)
}
if result.ModifiedFiles != 1 {
t.Errorf("expected 1 modified, got %d", result.ModifiedFiles)
}
}
func TestIsHiddenOrMeta(t *testing.T) {
tests := []struct {
path string
expected bool
}{
{"/vault/.verstak/config.yml", true},
{"/vault/.verstak/trash/file.txt", true},
{"/vault/my-project/file.txt", false},
{"/vault/.hidden/file.txt", true},
{"/vault/project/.hidden/file.txt", true},
{"/vault/project/file.txt", false},
}
for _, tc := range tests {
got := isHiddenOrMeta(tc.path)
if got != tc.expected {
t.Errorf("isHiddenOrMeta(%q) = %v, want %v", tc.path, got, tc.expected)
}
}
}