package storage import ( "context" "os" "path/filepath" "testing" "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" "github.com/dolthub/dolt/go/libraries/doltcore/ref" "github.com/dolthub/dolt/go/libraries/utils/earl" "github.com/dolthub/dolt/go/libraries/utils/filesys" "github.com/dolthub/dolt/go/store/types" ) // nbfVer is the NBS version string matching the format InitStore writes with. var nbfVer = types.Format_DOLT.VersionString() func TestRepoDiskPath(t *testing.T) { got := RepoDiskPath("/var/lib/dolt", "alice", "widgets") want := filepath.Join("/var/lib/dolt", "~alice", "widgets") if got != want { t.Fatalf("RepoDiskPath = %q, want %q", got, want) } } func TestInitStoreCreatesValidStore(t *testing.T) { ctx := context.Background() root := t.TempDir() absPath := RepoDiskPath(root, "alice", "widgets") if err := InitStore(ctx, absPath, "alice", "alice@example.test"); err != nil { t.Fatalf("InitStore: %v", err) } // Re-open the bare store read-only and confirm it is a real repo with a // "main" branch and a resolvable initial commit. fileURL := earl.FileUrlFromPath(absPath, os.PathSeparator) ddb, err := doltdb.LoadDoltDB(ctx, types.Format_DOLT, fileURL, filesys.LocalFS) if err != nil { t.Fatalf("re-open LoadDoltDB(%s): %v", fileURL, err) } // NOTE: this read-only verification handle is intentionally not closed. // Closing a DoltDB after resolving a commit trips an nbs table-index // ref-count panic upstream (store/nbs/table_index.go). The handle is // reclaimed at process exit and t.TempDir removes the files; this test only // needs to confirm the store is a valid, readable repo. if _, has, err := ddb.HasBranch(ctx, "main"); err != nil { t.Fatalf("HasBranch(main): %v", err) } else if !has { t.Fatalf("re-opened store has no 'main' branch") } commit, err := ddb.ResolveCommitRef(ctx, ref.NewBranchRef("main")) if err != nil { t.Fatalf("ResolveCommitRef(main): %v", err) } if commit == nil { t.Fatalf("ResolveCommitRef(main) returned nil commit") } if _, err := commit.HashOf(); err != nil { t.Fatalf("initial commit HashOf: %v", err) } } func TestInitStoreRejectsRelativePath(t *testing.T) { if err := InitStore(context.Background(), "relative/store", "alice", "a@b.test"); err == nil { t.Fatalf("InitStore accepted a relative path") } } func TestInitStorePartialFailureCleanup(t *testing.T) { if os.Geteuid() == 0 { t.Skip("running as root: permission-based failure cannot be induced") } ctx := context.Background() parent := t.TempDir() // A read-only intermediate directory makes MkdirAll fail so InitStore // cannot even create the store dir; assert nothing is left behind. ro := filepath.Join(parent, "ro") if err := os.Mkdir(ro, 0o555); err != nil { t.Fatal(err) } t.Cleanup(func() { os.Chmod(ro, 0o755) }) absPath := filepath.Join(ro, "sub", "store") if err := InitStore(ctx, absPath, "alice", "a@b.test"); err == nil { t.Fatalf("InitStore succeeded under a read-only parent") } if _, err := os.Stat(absPath); !os.IsNotExist(err) { t.Fatalf("expected no leftover store dir at %q, stat err = %v", absPath, err) } if _, err := os.Stat(filepath.Join(ro, "sub")); !os.IsNotExist(err) { t.Fatalf("expected no leftover intermediate dir, stat err = %v", err) } } func TestDeleteStoreRemovesStore(t *testing.T) { ctx := context.Background() root := t.TempDir() absPath := RepoDiskPath(root, "alice", "widgets") if err := InitStore(ctx, absPath, "alice", "a@b.test"); err != nil { t.Fatalf("InitStore: %v", err) } if err := DeleteStore(ctx, root, absPath); err != nil { t.Fatalf("DeleteStore: %v", err) } if _, err := os.Stat(absPath); !os.IsNotExist(err) { t.Fatalf("store dir still present after DeleteStore, stat err = %v", err) } } func TestDeleteStoreRootEscapeGuard(t *testing.T) { ctx := context.Background() root := t.TempDir() sibling := t.TempDir() // absolute, definitely outside root // A canary the guard must protect. canary := filepath.Join(sibling, "keep") if err := os.WriteFile(canary, []byte("x"), 0o644); err != nil { t.Fatal(err) } cases := map[string]string{ "sibling dir": sibling, "traversal escape": filepath.Join(root, "..", filepath.Base(sibling)), "the root itself": root, "prefix-not-subdir": root + "-evil", } for name, target := range cases { t.Run(name, func(t *testing.T) { if err := DeleteStore(ctx, root, target); err == nil { t.Fatalf("DeleteStore(%q) should have been refused", target) } }) } if _, err := os.Stat(canary); err != nil { t.Fatalf("canary disturbed by a refused DeleteStore: %v", err) } } func TestDeleteStoreRequiresAbsolute(t *testing.T) { ctx := context.Background() if err := DeleteStore(ctx, "rel/root", "/tmp/x"); err == nil { t.Fatalf("DeleteStore accepted a relative root") } if err := DeleteStore(ctx, "/tmp", "rel/path"); err == nil { t.Fatalf("DeleteStore accepted a relative path") } } func TestNewCacheRejectsNilLookup(t *testing.T) { defer func() { if recover() == nil { t.Fatalf("NewCache(nil) did not panic") } }() NewCache(nil) } func TestCacheGetHitAndMemoize(t *testing.T) { ctx := context.Background() root := t.TempDir() absPath := RepoDiskPath(root, "alice", "widgets") if err := InitStore(ctx, absPath, "alice", "a@b.test"); err != nil { t.Fatalf("InitStore: %v", err) } var lookups int cache := NewCache(func(_ context.Context, owner, name string) (string, error) { lookups++ if owner == "alice" && name == "widgets" { return absPath, nil } return "", os.ErrNotExist }) defer cache.Close() cs1, err := cache.Get(ctx, "~alice/widgets", nbfVer) if err != nil { t.Fatalf("Get (first): %v", err) } cs2, err := cache.Get(ctx, "alice/widgets", nbfVer) if err != nil { t.Fatalf("Get (second): %v", err) } if cs1 != cs2 { t.Fatalf("expected memoized store to be identical across calls") } if lookups != 2 { t.Fatalf("expected 2 lookups (memoization keys on disk path, not the call), got %d", lookups) } } func TestCacheGetMiss(t *testing.T) { ctx := context.Background() cache := NewCache(func(_ context.Context, owner, name string) (string, error) { return "", os.ErrNotExist }) defer cache.Close() if _, err := cache.Get(ctx, "~ghost/missing", nbfVer); err == nil { t.Fatalf("Get for an unknown repo should error") } } func TestCacheGetRejectsBadPath(t *testing.T) { ctx := context.Background() called := false cache := NewCache(func(_ context.Context, owner, name string) (string, error) { called = true return "", nil }) defer cache.Close() if _, err := cache.Get(ctx, "no-slash", nbfVer); err == nil { t.Fatalf("Get with a malformed path should error before lookup") } if called { t.Fatalf("RepoLookup was consulted for a structurally invalid path") } } func TestCacheEvictReopens(t *testing.T) { ctx := context.Background() root := t.TempDir() absPath := RepoDiskPath(root, "alice", "widgets") if err := InitStore(ctx, absPath, "alice", "a@b.test"); err != nil { t.Fatalf("InitStore: %v", err) } cache := NewCache(func(_ context.Context, owner, name string) (string, error) { return absPath, nil }) defer cache.Close() cs1, err := cache.Get(ctx, "~alice/widgets", nbfVer) if err != nil { t.Fatalf("Get (first): %v", err) } if err := cache.Evict(absPath); err != nil { t.Fatalf("Evict: %v", err) } // Evicting an absent path is a no-op. if err := cache.Evict(absPath); err != nil { t.Fatalf("Evict (absent) should be a no-op, got %v", err) } cs2, err := cache.Get(ctx, "~alice/widgets", nbfVer) if err != nil { t.Fatalf("Get after evict: %v", err) } if cs1 == cs2 { t.Fatalf("expected a freshly opened store after eviction, got the evicted handle") } }