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")
}
}