@@ -290,6 +290,240 @@ func TestCpJSONErrorUsesCommandStderr(t *testing.T) {
290290 }
291291}
292292
293+ func TestCpCommandDefinesIfExistsFlag (t * testing.T ) {
294+ flag := cpCmd .Flags ().Lookup ("if-exists" )
295+ if flag == nil {
296+ t .Fatal ("cp should define --if-exists" )
297+ }
298+ if flag .DefValue != relocationIfExistsFail {
299+ t .Fatalf ("--if-exists default = %q, want %q" , flag .DefValue , relocationIfExistsFail )
300+ }
301+ }
302+
303+ func TestCpInvalidIfExistsReturnsInvalidArguments (t * testing.T ) {
304+ var stdout bytes.Buffer
305+ cmd := newRelocationTestCommand (& stdout , nil )
306+ if err := cmd .Flags ().Set ("if-exists" , "replace" ); err != nil {
307+ t .Fatal (err )
308+ }
309+
310+ err := cp (cmd , []string {"/src/file.txt" , "/dest/file.txt" })
311+ if err == nil {
312+ t .Fatal ("expected cp error" )
313+ }
314+ if code := jsonErrorCode (err ); code != jsonErrorCodeInvalidArguments {
315+ t .Fatalf ("json error code = %q, want %q" , code , jsonErrorCodeInvalidArguments )
316+ }
317+ details := jsonErrorDetails (err )
318+ if details ["flag" ] != "if-exists" || details ["value" ] != "replace" {
319+ t .Fatalf ("details = %#v, want if-exists flag value" , details )
320+ }
321+ if stdout .String () != "" {
322+ t .Fatalf ("stdout = %q, want empty" , stdout .String ())
323+ }
324+ }
325+
326+ func TestCpIfExistsFailCallsCopy (t * testing.T ) {
327+ var copied []* files.RelocationArg
328+ stubFilesClient (t , & mockFilesClient {
329+ getMetadataFn : func (arg * files.GetMetadataArg ) (files.IsMetadata , error ) {
330+ return nil , relocationTestGetMetadataNotFoundError ()
331+ },
332+ copyV2Fn : func (arg * files.RelocationArg ) (* files.RelocationResult , error ) {
333+ copied = append (copied , arg )
334+ return files .NewRelocationResult (relocationTestFileMetadata (arg .ToPath , 1 )), nil
335+ },
336+ })
337+
338+ var stdout bytes.Buffer
339+ cmd := newRelocationTestCommand (& stdout , nil )
340+ if err := cmd .Flags ().Set ("if-exists" , relocationIfExistsFail ); err != nil {
341+ t .Fatal (err )
342+ }
343+ if err := cp (cmd , []string {"/src/file.txt" , "/dest/file.txt" }); err != nil {
344+ t .Fatalf ("cp error: %v" , err )
345+ }
346+ if len (copied ) != 1 {
347+ t .Fatalf ("copied = %d, want 1" , len (copied ))
348+ }
349+ }
350+
351+ func TestCpIfExistsSkipExistingDestinationDoesNotCopy (t * testing.T ) {
352+ var copied bool
353+ stubFilesClient (t , & mockFilesClient {
354+ getMetadataFn : func (arg * files.GetMetadataArg ) (files.IsMetadata , error ) {
355+ if arg .Path != "/dest/file.txt" {
356+ t .Fatalf ("metadata path = %q, want /dest/file.txt" , arg .Path )
357+ }
358+ return relocationTestFileMetadata (arg .Path , 8 ), nil
359+ },
360+ copyV2Fn : func (arg * files.RelocationArg ) (* files.RelocationResult , error ) {
361+ copied = true
362+ return nil , nil
363+ },
364+ })
365+
366+ var stdout bytes.Buffer
367+ cmd := newRelocationTestCommand (& stdout , nil )
368+ if err := cmd .Flags ().Set ("if-exists" , relocationIfExistsSkip ); err != nil {
369+ t .Fatal (err )
370+ }
371+
372+ if err := cp (cmd , []string {"/src/file.txt" , "/dest/file.txt" }); err != nil {
373+ t .Fatalf ("cp error: %v" , err )
374+ }
375+ if copied {
376+ t .Fatal ("CopyV2 called for skipped destination" )
377+ }
378+ got := decodeRelocationOutput (t , stdout .Bytes ())
379+ if len (got .Results ) != 1 {
380+ t .Fatalf ("results = %d, want 1" , len (got .Results ))
381+ }
382+ if got .Results [0 ].Status != relocationJSONStatusSkipped {
383+ t .Fatalf ("status = %q, want skipped" , got .Results [0 ].Status )
384+ }
385+ if got .Results [0 ].Input .FromPath != "/src/file.txt" || got .Results [0 ].Input .ToPath != "/dest/file.txt" {
386+ t .Fatalf ("input = %#v, want source and destination" , got .Results [0 ].Input )
387+ }
388+ }
389+
390+ func TestCpIfExistsSkipMissingDestinationCopies (t * testing.T ) {
391+ var copied []* files.RelocationArg
392+ stubFilesClient (t , & mockFilesClient {
393+ getMetadataFn : func (arg * files.GetMetadataArg ) (files.IsMetadata , error ) {
394+ return nil , relocationTestGetMetadataNotFoundError ()
395+ },
396+ copyV2Fn : func (arg * files.RelocationArg ) (* files.RelocationResult , error ) {
397+ copied = append (copied , arg )
398+ return files .NewRelocationResult (relocationTestFileMetadata (arg .ToPath , 3 )), nil
399+ },
400+ })
401+
402+ var stdout bytes.Buffer
403+ cmd := newRelocationTestCommand (& stdout , nil )
404+ if err := cmd .Flags ().Set ("if-exists" , relocationIfExistsSkip ); err != nil {
405+ t .Fatal (err )
406+ }
407+
408+ if err := cp (cmd , []string {"/src/file.txt" , "/dest/file.txt" }); err != nil {
409+ t .Fatalf ("cp error: %v" , err )
410+ }
411+ if len (copied ) != 1 {
412+ t .Fatalf ("copied = %d, want 1" , len (copied ))
413+ }
414+ got := decodeRelocationOutput (t , stdout .Bytes ())
415+ if got .Results [0 ].Status != relocationJSONStatusCopied {
416+ t .Fatalf ("status = %q, want copied" , got .Results [0 ].Status )
417+ }
418+ }
419+
420+ func TestCpIfExistsSkipConvertsDestinationConflict (t * testing.T ) {
421+ getMetadataCalls := 0
422+ stubFilesClient (t , & mockFilesClient {
423+ getMetadataFn : func (arg * files.GetMetadataArg ) (files.IsMetadata , error ) {
424+ getMetadataCalls ++
425+ if getMetadataCalls < 3 {
426+ return nil , relocationTestGetMetadataNotFoundError ()
427+ }
428+ return relocationTestFileMetadata (arg .Path , 13 ), nil
429+ },
430+ copyV2Fn : func (arg * files.RelocationArg ) (* files.RelocationResult , error ) {
431+ return nil , relocationTestCopyDestinationConflictError ()
432+ },
433+ })
434+
435+ var stdout bytes.Buffer
436+ cmd := newRelocationTestCommand (& stdout , nil )
437+ if err := cmd .Flags ().Set ("if-exists" , relocationIfExistsSkip ); err != nil {
438+ t .Fatal (err )
439+ }
440+
441+ if err := cp (cmd , []string {"/src/file.txt" , "/dest/file.txt" }); err != nil {
442+ t .Fatalf ("cp error: %v" , err )
443+ }
444+ got := decodeRelocationOutput (t , stdout .Bytes ())
445+ if len (got .Results ) != 1 {
446+ t .Fatalf ("results = %d, want 1" , len (got .Results ))
447+ }
448+ if got .Results [0 ].Status != relocationJSONStatusSkipped {
449+ t .Fatalf ("status = %q, want skipped" , got .Results [0 ].Status )
450+ }
451+ }
452+
453+ func TestCpIfExistsSkipMultipleSourcesAppliesPerTarget (t * testing.T ) {
454+ var copied []string
455+ stubFilesClient (t , & mockFilesClient {
456+ getMetadataFn : func (arg * files.GetMetadataArg ) (files.IsMetadata , error ) {
457+ switch arg .Path {
458+ case "/dest/a.txt" :
459+ return relocationTestFileMetadata (arg .Path , 1 ), nil
460+ case "/dest/b.txt" :
461+ return nil , relocationTestGetMetadataNotFoundError ()
462+ default :
463+ t .Fatalf ("unexpected metadata path %q" , arg .Path )
464+ return nil , nil
465+ }
466+ },
467+ copyV2Fn : func (arg * files.RelocationArg ) (* files.RelocationResult , error ) {
468+ copied = append (copied , arg .ToPath )
469+ return files .NewRelocationResult (relocationTestFileMetadata (arg .ToPath , 2 )), nil
470+ },
471+ })
472+
473+ var stdout bytes.Buffer
474+ cmd := newRelocationTestCommand (& stdout , nil )
475+ if err := cmd .Flags ().Set ("if-exists" , relocationIfExistsSkip ); err != nil {
476+ t .Fatal (err )
477+ }
478+
479+ if err := cp (cmd , []string {"/src/a.txt" , "/src/b.txt" , "/dest" }); err != nil {
480+ t .Fatalf ("cp error: %v" , err )
481+ }
482+ if len (copied ) != 1 || copied [0 ] != "/dest/b.txt" {
483+ t .Fatalf ("copied = %#v, want only /dest/b.txt" , copied )
484+ }
485+ got := decodeRelocationOutput (t , stdout .Bytes ())
486+ if len (got .Results ) != 2 {
487+ t .Fatalf ("results = %d, want 2" , len (got .Results ))
488+ }
489+ if got .Results [0 ].Status != relocationJSONStatusSkipped || got .Results [1 ].Status != relocationJSONStatusCopied {
490+ t .Fatalf ("statuses = %q, %q; want skipped, copied" , got .Results [0 ].Status , got .Results [1 ].Status )
491+ }
492+ }
493+
494+ func TestCpIfExistsSkipTextModeQuiet (t * testing.T ) {
495+ stubFilesClient (t , & mockFilesClient {
496+ getMetadataFn : func (arg * files.GetMetadataArg ) (files.IsMetadata , error ) {
497+ return relocationTestFileMetadata (arg .Path , 8 ), nil
498+ },
499+ copyV2Fn : func (arg * files.RelocationArg ) (* files.RelocationResult , error ) {
500+ t .Fatal ("CopyV2 called for skipped destination" )
501+ return nil , nil
502+ },
503+ })
504+
505+ var stdout bytes.Buffer
506+ var stderr bytes.Buffer
507+ cmd := & cobra.Command {}
508+ cmd .Flags ().String (outputFlag , string (output .FormatText ), "" )
509+ cmd .Flags ().String ("if-exists" , relocationIfExistsFail , "" )
510+ if err := cmd .Flags ().Set ("if-exists" , relocationIfExistsSkip ); err != nil {
511+ t .Fatal (err )
512+ }
513+ cmd .SetOut (& stdout )
514+ cmd .SetErr (& stderr )
515+
516+ if err := cp (cmd , []string {"/src/file.txt" , "/dest/file.txt" }); err != nil {
517+ t .Fatalf ("cp error: %v" , err )
518+ }
519+ if stdout .String () != "" {
520+ t .Fatalf ("stdout = %q, want empty" , stdout .String ())
521+ }
522+ if stderr .String () != "" {
523+ t .Fatalf ("stderr = %q, want empty" , stderr .String ())
524+ }
525+ }
526+
293527func TestCpCommandSupportsStructuredOutput (t * testing.T ) {
294528 if ! commandSupportsStructuredOutput (cpCmd ) {
295529 t .Fatal ("cp should support structured output" )
@@ -299,6 +533,7 @@ func TestCpCommandSupportsStructuredOutput(t *testing.T) {
299533func newRelocationTestCommand (stdout , stderr * bytes.Buffer ) * cobra.Command {
300534 cmd := & cobra.Command {}
301535 cmd .Flags ().String (outputFlag , string (output .FormatText ), "" )
536+ cmd .Flags ().String ("if-exists" , relocationIfExistsFail , "" )
302537 if err := cmd .Flags ().Set (outputFlag , string (output .FormatJSON )); err != nil {
303538 panic (err )
304539 }
@@ -312,9 +547,16 @@ func newRelocationTestCommand(stdout, stderr *bytes.Buffer) *cobra.Command {
312547}
313548
314549type relocationOutput struct {
315- Input map [string ]any `json:"input"`
316- Results []relocationResult `json:"results"`
317- Warnings []jsonWarning `json:"warnings"`
550+ Input map [string ]any `json:"input"`
551+ Results []relocationJSONResult `json:"results"`
552+ Warnings []jsonWarning `json:"warnings"`
553+ }
554+
555+ type relocationJSONResult struct {
556+ Status string `json:"status"`
557+ Kind string `json:"kind"`
558+ Input relocationInput `json:"input"`
559+ Result jsonMetadata `json:"result"`
318560}
319561
320562func decodeRelocationOutput (t * testing.T , data []byte ) relocationOutput {
@@ -337,3 +579,41 @@ func decodeRelocationOutput(t *testing.T, data []byte) relocationOutput {
337579 }
338580 return got
339581}
582+
583+ func relocationTestFileMetadata (pathDisplay string , size uint64 ) * files.FileMetadata {
584+ metadata := files .NewFileMetadata (path .Base (pathDisplay ), "id:" + path .Base (pathDisplay ), time.Time {}, time.Time {}, "rev" , size )
585+ metadata .PathDisplay = pathDisplay
586+ metadata .PathLower = strings .ToLower (pathDisplay )
587+ return metadata
588+ }
589+
590+ func relocationTestGetMetadataNotFoundError () error {
591+ return files.GetMetadataAPIError {
592+ EndpointError : & files.GetMetadataError {
593+ Tagged : dropbox.Tagged {Tag : files .GetMetadataErrorPath },
594+ Path : & files.LookupError {Tagged : dropbox.Tagged {Tag : files .LookupErrorNotFound }},
595+ },
596+ }
597+ }
598+
599+ func relocationTestCopyDestinationConflictError () error {
600+ return files.CopyV2APIError {
601+ EndpointError : relocationTestDestinationConflictError (),
602+ }
603+ }
604+
605+ func relocationTestMoveDestinationConflictError () error {
606+ return files.MoveV2APIError {
607+ EndpointError : relocationTestDestinationConflictError (),
608+ }
609+ }
610+
611+ func relocationTestDestinationConflictError () * files.RelocationError {
612+ return & files.RelocationError {
613+ Tagged : dropbox.Tagged {Tag : files .RelocationErrorTo },
614+ To : & files.WriteError {
615+ Tagged : dropbox.Tagged {Tag : files .WriteErrorConflict },
616+ Conflict : & files.WriteConflictError {Tagged : dropbox.Tagged {Tag : files .WriteConflictErrorFile }},
617+ },
618+ }
619+ }
0 commit comments