diff --git a/cmd/mock_test.go b/cmd/mock_test.go index 977e890..f915a25 100644 --- a/cmd/mock_test.go +++ b/cmd/mock_test.go @@ -22,6 +22,8 @@ type mockFilesClient struct { listFolderContinueFn func(arg *files.ListFolderContinueArg) (*files.ListFolderResult, error) moveV2Fn func(arg *files.RelocationArg) (*files.RelocationResult, error) permanentlyDeleteFn func(arg *files.DeleteArg) error + searchV2Fn func(arg *files.SearchV2Arg) (*files.SearchV2Result, error) + searchContinueV2Fn func(arg *files.SearchV2ContinueArg) (*files.SearchV2Result, error) } func (m *mockFilesClient) Download(arg *files.DownloadArg) (*files.FileMetadata, io.ReadCloser, error) { @@ -242,9 +244,15 @@ func (m *mockFilesClient) Search(arg *files.SearchArg) (*files.SearchResult, err return nil, nil } func (m *mockFilesClient) SearchV2(arg *files.SearchV2Arg) (*files.SearchV2Result, error) { + if m.searchV2Fn != nil { + return m.searchV2Fn(arg) + } return nil, nil } func (m *mockFilesClient) SearchContinueV2(arg *files.SearchV2ContinueArg) (*files.SearchV2Result, error) { + if m.searchContinueV2Fn != nil { + return m.searchContinueV2Fn(arg) + } return nil, nil } func (m *mockFilesClient) TagsAdd(arg *files.AddTagArg) error { return nil } diff --git a/cmd/search.go b/cmd/search.go index b1df526..e9a646f 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -18,7 +18,6 @@ import ( "errors" "fmt" "io" - "os" "strings" "text/tabwriter" @@ -31,7 +30,6 @@ func search(cmd *cobra.Command, args []string) (err error) { return errors.New("`search` requires a `query` argument") } - // Parse path scope, if provided. var scope string if len(args) == 2 { scope = args[1] @@ -40,20 +38,48 @@ func search(cmd *cobra.Command, args []string) (err error) { } } - arg := files.NewSearchArg(scope, args[0]) + arg := files.NewSearchV2Arg(args[0]) + if scope != "" { + opts := files.NewSearchOptions() + opts.Path = scope + arg.Options = opts + } - dbx := files.New(config) - res, err := dbx.Search(arg) + dbx := filesNewFunc(config) + res, err := dbx.SearchV2(arg) if err != nil { - return + return err + } + + var entries []files.IsMetadata + for _, m := range res.Matches { + if m.Metadata != nil && m.Metadata.Metadata != nil { + entries = append(entries, m.Metadata.Metadata) + } + } + + for res.HasMore { + contArg := files.NewSearchV2ContinueArg(res.Cursor) + res, err = dbx.SearchContinueV2(contArg) + if err != nil { + return err + } + for _, m := range res.Matches { + if m.Metadata != nil && m.Metadata.Metadata != nil { + entries = append(entries, m.Metadata.Metadata) + } + } } opts := parseLsOptions(cmd) + sortEntries(entries, opts) - return renderSearchResults(os.Stdout, res, opts) + return commandOutput(cmd).RenderText(func(w io.Writer) error { + return renderSearchResults(w, entries, opts) + }) } -func renderSearchResults(out io.Writer, res *files.SearchResult, opts listOptions) error { +func renderSearchResults(out io.Writer, entries []files.IsMetadata, opts listOptions) error { w := new(tabwriter.Writer) w.Init(out, 4, 8, 1, ' ', 0) @@ -61,12 +87,6 @@ func renderSearchResults(out io.Writer, res *files.SearchResult, opts listOption _, _ = fmt.Fprint(w, "Revision\tSize\tLast modified\tPath\n") } - entries := make([]files.IsMetadata, 0, len(res.Matches)) - for _, m := range res.Matches { - entries = append(entries, m.Metadata) - } - sortEntries(entries, opts) - for _, entry := range entries { switch f := entry.(type) { case *files.FileMetadata: diff --git a/cmd/search_test.go b/cmd/search_test.go index b8f80f4..1a67edc 100644 --- a/cmd/search_test.go +++ b/cmd/search_test.go @@ -5,7 +5,9 @@ import ( "strings" "testing" + "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files" + "github.com/spf13/cobra" ) func TestSearchArgValidation(t *testing.T) { @@ -23,17 +25,17 @@ func TestSearchPathScopeValidation(t *testing.T) { } func TestRenderSearchResultsSeparatesMatchesWithNewlines(t *testing.T) { - res := files.NewSearchResult([]*files.SearchMatch{ - files.NewSearchMatch(nil, &files.FileMetadata{ + entries := []files.IsMetadata{ + &files.FileMetadata{ Metadata: files.Metadata{PathDisplay: "/first.txt"}, - }), - files.NewSearchMatch(nil, &files.FolderMetadata{ + }, + &files.FolderMetadata{ Metadata: files.Metadata{PathDisplay: "/second"}, - }), - }, false, 0) + }, + } var out bytes.Buffer - if err := renderSearchResults(&out, res, listOptions{long: false}); err != nil { + if err := renderSearchResults(&out, entries, listOptions{long: false}); err != nil { t.Fatalf("renderSearchResults returned error: %v", err) } @@ -49,3 +51,96 @@ func TestRenderSearchResultsSeparatesMatchesWithNewlines(t *testing.T) { t.Errorf("second rendered match = %q, want %q", got, want) } } + +func TestRenderSearchResultsLongModeIncludesHeader(t *testing.T) { + entries := []files.IsMetadata{ + &files.FileMetadata{ + Metadata: files.Metadata{PathDisplay: "/first.txt"}, + Rev: "abc123", + Size: 42, + }, + } + + var out bytes.Buffer + if err := renderSearchResults(&out, entries, listOptions{long: true}); err != nil { + t.Fatalf("renderSearchResults returned error: %v", err) + } + + got := out.String() + for _, want := range []string{"Revision", "Size", "Last modified", "Path", "abc123", "/first.txt"} { + if !strings.Contains(got, want) { + t.Errorf("output = %q, want to contain %q", got, want) + } + } +} + +func TestSearchUsesSearchV2AndCommandOutput(t *testing.T) { + cmd, stdout := testSearchCmd() + var firstArg *files.SearchV2Arg + var continueCursor string + + mock := &mockFilesClient{ + searchV2Fn: func(arg *files.SearchV2Arg) (*files.SearchV2Result, error) { + firstArg = arg + res := files.NewSearchV2Result([]*files.SearchMatchV2{ + searchMatch(&files.FileMetadata{ + Metadata: files.Metadata{PathDisplay: "/docs/first.txt"}, + }), + }, true) + res.Cursor = "cursor-1" + return res, nil + }, + searchContinueV2Fn: func(arg *files.SearchV2ContinueArg) (*files.SearchV2Result, error) { + continueCursor = arg.Cursor + return files.NewSearchV2Result([]*files.SearchMatchV2{ + searchMatch(&files.FolderMetadata{ + Metadata: files.Metadata{PathDisplay: "/docs/second"}, + }), + }, false), nil + }, + } + stubFilesClient(t, mock) + + if err := search(cmd, []string{"needle", "/docs"}); err != nil { + t.Fatalf("search error: %v", err) + } + + if firstArg == nil { + t.Fatal("SearchV2 was not called") + } + if firstArg.Query != "needle" { + t.Errorf("query = %q, want %q", firstArg.Query, "needle") + } + if firstArg.Options == nil || firstArg.Options.Path != "/docs" { + t.Fatalf("options path = %#v, want /docs", firstArg.Options) + } + if continueCursor != "cursor-1" { + t.Errorf("continue cursor = %q, want cursor-1", continueCursor) + } + + got := stdout.String() + for _, want := range []string{"/docs/first.txt", "/docs/second"} { + if !strings.Contains(got, want) { + t.Errorf("stdout = %q, want to contain %q", got, want) + } + } +} + +func testSearchCmd() (*cobra.Command, *bytes.Buffer) { + var stdout bytes.Buffer + cmd := &cobra.Command{Use: "search"} + cmd.SetOut(&stdout) + cmd.Flags().BoolP("long", "l", false, "") + cmd.Flags().String("sort", "", "") + cmd.Flags().BoolP("reverse", "r", false, "") + cmd.Flags().String("time", "server", "") + cmd.Flags().String("time-format", "", "") + return cmd, &stdout +} + +func searchMatch(metadata files.IsMetadata) *files.SearchMatchV2 { + return files.NewSearchMatchV2(&files.MetadataV2{ + Tagged: dropbox.Tagged{Tag: files.MetadataV2Metadata}, + Metadata: metadata, + }) +}