package storage_test import ( "context" "errors" "io/fs" "os" "path/filepath" "reflect" "strings" "testing" "sneak.berlin/go/vaultik/internal/storage" ) // newFileStorer builds a file:// backend rooted at a fresh temp directory. // //nolint:ireturn // conformance runs against the Storer interface by design func newFileStorer(t *testing.T) storage.Storer { t.Helper() s, err := storage.NewFileStorer(t.TempDir()) if err != nil { t.Fatalf("NewFileStorer: %v", err) } return s } // TestFileStorer runs the shared Storer contract against the file:// backend. func TestFileStorer(t *testing.T) { t.Parallel() runStorerConformance(t, newFileStorer) } // TestFileStorerListMissingDestination checks that List and ListStream // fail when the destination directory does not exist, instead of // reporting an empty store. func TestFileStorerListMissingDestination(t *testing.T) { t.Parallel() ctx := context.Background() s, err := storage.NewFileStorer(filepath.Join(t.TempDir(), "unmounted")) if err != nil { t.Fatalf("NewFileStorer: %v", err) } keys, err := s.List(ctx, "metadata/") if !errors.Is(err, fs.ErrNotExist) { t.Errorf("List = %v, %v; want a not-exist error", keys, err) } var streamErr error for object := range s.ListStream(ctx, "metadata/") { if object.Err != nil { streamErr = object.Err } } if !errors.Is(streamErr, fs.ErrNotExist) { t.Errorf("ListStream error = %v, want a not-exist error", streamErr) } } // TestFileStorerListsThroughSymlinkedDirectory checks that List and // ListStream read through a symlinked metadata/ or blobs/ directory, as Get // and Stat do, instead of listing the link as one key. Listing the whole // destination meets the link part-way through the walk; listing its prefix // starts the walk at the link. func TestFileStorerListsThroughSymlinkedDirectory(t *testing.T) { t.Parallel() for _, dir := range []string{"metadata", "blobs"} { t.Run(dir, func(t *testing.T) { t.Parallel() base := t.TempDir() key := dir + "/aa/bb/object" s, err := storage.NewFileStorer(base) if err != nil { t.Fatalf("NewFileStorer: %v", err) } err = s.Put(context.Background(), key, strings.NewReader("data")) if err != nil { t.Fatalf("Put: %v", err) } linkPath := filepath.Join(base, dir) movedPath := filepath.Join(t.TempDir(), dir) err = os.Rename(linkPath, movedPath) if err != nil { t.Fatalf("moving %s away: %v", dir, err) } err = os.Symlink(movedPath, linkPath) if err != nil { t.Fatalf("linking %s: %v", dir, err) } want := []string{key} for _, prefix := range []string{"", dir + "/"} { got := listSorted(t, s, prefix) if !reflect.DeepEqual(got, want) { t.Errorf("List(%q) = %v, want %v", prefix, got, want) } got = listStreamKeys(t, s, prefix) if !reflect.DeepEqual(got, want) { t.Errorf("ListStream(%q) = %v, want %v", prefix, got, want) } } }) } } // TestFileStorerListFailsOnSymlinkWithMissingTarget checks that List and // ListStream fail on a symlinked metadata/ or blobs/ directory whose target // is missing, such as a link to a volume that is not mounted, as they do on // a missing destination directory, instead of listing it as empty. func TestFileStorerListFailsOnSymlinkWithMissingTarget(t *testing.T) { t.Parallel() ctx := context.Background() for _, dir := range []string{"metadata", "blobs"} { t.Run(dir, func(t *testing.T) { t.Parallel() base := t.TempDir() s, err := storage.NewFileStorer(base) if err != nil { t.Fatalf("NewFileStorer: %v", err) } unmounted := filepath.Join(t.TempDir(), "unmounted") err = os.Symlink(unmounted, filepath.Join(base, dir)) if err != nil { t.Fatalf("Symlink: %v", err) } for _, prefix := range []string{"", dir + "/"} { keys, err := s.List(ctx, prefix) if !errors.Is(err, fs.ErrNotExist) { t.Errorf("List(%q) = %v, %v; want a not-exist error", prefix, keys, err) } var streamErr error for object := range s.ListStream(ctx, prefix) { if object.Err != nil { streamErr = object.Err } } if !errors.Is(streamErr, fs.ErrNotExist) { t.Errorf("ListStream(%q) error = %v, want a not-exist error", prefix, streamErr) } } }) } } // TestFileStorerListFailsOnSymlinkLoop checks that a symlink pointing back // at the destination makes List and ListStream fail instead of walking it // forever. func TestFileStorerListFailsOnSymlinkLoop(t *testing.T) { t.Parallel() ctx := context.Background() base := t.TempDir() s, err := storage.NewFileStorer(base) if err != nil { t.Fatalf("NewFileStorer: %v", err) } err = s.Put(ctx, testBlobKey, strings.NewReader("data")) if err != nil { t.Fatalf("Put: %v", err) } err = os.Symlink(base, filepath.Join(base, "blobs", "loop")) if err != nil { t.Fatalf("Symlink: %v", err) } keys, err := s.List(ctx, "blobs/") if err == nil { t.Errorf("List = %d keys and no error, want an error", len(keys)) } var streamErr error for object := range s.ListStream(ctx, "blobs/") { if object.Err != nil { streamErr = object.Err } } if streamErr == nil { t.Error("ListStream sent no error, want one") } }