Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/help_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{
Args: []jsonCommandArg{commandArg("url", true, false, "url", "Shared link URL")},
Examples: []jsonCommandExample{{Description: "Set a shared link audience", Command: "dbxcli share-link update https://www.dropbox.com/s/example/file.txt --audience team"}},
Flags: mergeCommandFlagMetadata(mergeCommandFlagMetadata(sharedLinkSettingsFlagMetadata, sharedLinkPasswordFlagMetadata), map[string]jsonCommandFlagMetadata{
dryRunFlagName: {ValueKind: "boolean"},
"password": {Conflicts: []string{"password-file", "password-prompt", "remove-password"}, Sensitive: true, ValueKind: "secret"},
"password-file": {Conflicts: []string{"password", "password-prompt", "remove-password"}, ValueKind: "local_file"},
"password-prompt": {Conflicts: []string{"password", "password-file", "remove-password"}, MayPrompt: true, ValueKind: "boolean"},
Expand Down Expand Up @@ -364,7 +365,7 @@ var commandContractRegistry = map[string]jsonCommandContractMetadata{
"share-link info": {Statuses: []string{"found"}, Kinds: []string{"file", "folder", "link"}},
"share-link list": {Statuses: []string{"listed"}, Kinds: []string{"file", "folder", "link"}},
"share-link revoke": {Statuses: []string{"revoked", jsonStatusPlanned}, Kinds: []string{"file", "folder", "link", "shared_link"}},
"share-link update": {Statuses: []string{"updated"}, Kinds: []string{"file", "folder", "link"}},
"share-link update": {Statuses: []string{"updated", jsonStatusPlanned}, Kinds: []string{"file", "folder", "link"}},
"team add-member": {Statuses: []string{"added", "completed", "started"}, Kinds: []string{"team_member"}},
"team info": {Statuses: []string{"found"}, Kinds: []string{"team"}},
"team list-groups": {Statuses: []string{"listed"}, Kinds: []string{"team_group"}},
Expand Down
7 changes: 4 additions & 3 deletions cmd/json_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -775,8 +775,8 @@ func jsonGoldenSuccessOutputExamples() map[string]jsonOperationOutput {
"share-link revoke": newJSONOperationOutput(shareLinkRevokeInput{Path: "/Reports/old.pdf", DryRun: false}, []jsonOperationResult{
newJSONOperationResult(shareLinkJSONStatusRevoked, sharedLink.Type, shareLinkRevokeResultInput{DryRun: false}, shareLinkRevokeResult{URL: sharedLink.URL, Link: &sharedLink}),
}, nil),
"share-link update": newJSONOperationOutput(shareLinkUpdateInput{URL: sharedLink.URL, Audience: "public", Expires: "2026-07-01T00:00:00Z", RemoveExpiration: false, AllowDownload: true, DisallowDownload: false, Password: true, RemovePassword: false}, []jsonOperationResult{
shareLinkJSONOperationResult(shareLinkJSONStatusUpdated, sharedLink),
"share-link update": newJSONOperationOutput(shareLinkUpdateInput{URL: sharedLink.URL, Audience: "public", Expires: "2026-07-01T00:00:00Z", RemoveExpiration: false, AllowDownload: true, DisallowDownload: false, Password: true, RemovePassword: false, DryRun: false}, []jsonOperationResult{
shareLinkUpdateOperationResult(shareLinkJSONStatusUpdated, sharedLink, shareLinkUpdateOptions{dryRun: false}),
}, nil),
"team add-member": newJSONOperationOutput(teamMemberAddInput{Email: "ada@example.com", FirstName: "Ada", LastName: "Lovelace"}, []jsonOperationResult{
newJSONOperationResult(teamJSONStatusAdded, teamJSONKindTeamMember, teamMemberAddInput{Email: "ada@example.com", FirstName: "Ada", LastName: "Lovelace"}, teamMemberMutationJSON{
Expand Down Expand Up @@ -1073,6 +1073,7 @@ func jsonContractDefinitions() map[string][]string {
"share_link_revoke_result_input": jsonFieldNames[shareLinkRevokeResultInput](),
"share_link_revoke_result": jsonFieldNames[shareLinkRevokeResult](),
"share_link_update_input": jsonFieldNames[shareLinkUpdateInput](),
"share_link_update_result_input": jsonFieldNames[shareLinkUpdateResultInput](),
"team_group": jsonFieldNames[teamGroupJSON](),
"team_info": jsonFieldNames[teamInfoJSON](),
"team_member": jsonFieldNames[teamMemberJSON](),
Expand Down Expand Up @@ -1114,7 +1115,7 @@ func jsonCommandSchemas() map[string]jsonGoldenCommandSchema {
"share-link info": operationSchema("share_link_info_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusFound}, shareLinkKinds(), nil),
"share-link list": operationSchema("share_link_list_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusListed}, shareLinkKinds(), nil),
"share-link revoke": operationSchema("share_link_revoke_input", schemaRef("share_link_revoke_result_input"), "share_link_revoke_result", []string{shareLinkJSONStatusRevoked, jsonStatusPlanned}, append(shareLinkKinds(), shareLinkJSONKindSharedLink), nil),
"share-link update": operationSchema("share_link_update_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusUpdated}, shareLinkKinds(), nil),
"share-link update": operationSchema("share_link_update_input", schemaRef("share_link_update_result_input"), "share_link_metadata", []string{shareLinkJSONStatusUpdated, jsonStatusPlanned}, shareLinkKinds(), nil),
"team add-member": operationSchema("team_member_add_input", schemaRef("team_member_add_input"), "team_member_mutation", []string{teamJSONStatusAdded, teamJSONStatusCompleted, teamJSONStatusStarted}, []string{teamJSONKindTeamMember}, nil),
"team info": operationSchema("empty", schemaRef("empty"), "team_info", []string{teamJSONStatusFound}, []string{teamJSONKindTeam}, nil),
"team list-groups": operationSchema("empty", schemaRef("empty"), "team_group", []string{teamJSONStatusListed}, []string{teamJSONKindTeamGroup}, nil),
Expand Down
1 change: 1 addition & 0 deletions cmd/share_link_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const (
shareLinkJSONStatusRevoked = "revoked"
shareLinkJSONStatusUpdated = "updated"

shareLinkJSONKindLink = "link"
shareLinkJSONKindSharedLink = "shared_link"
)

Expand Down
57 changes: 57 additions & 0 deletions cmd/share_link_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,63 @@ func TestShareLinkUpdateJSONOutputsUpdatedMetadata(t *testing.T) {
}
}

func TestShareLinkUpdateJSONDryRunOutputsPlannedUpdate(t *testing.T) {
stubSharedLinkClient(t, &mockSharedLinkClient{
modifySharedLinkSettingsFn: func(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error) {
t.Fatalf("ModifySharedLinkSettings called during dry-run: %v", arg)
return nil, nil
},
modifySharedLinkSettingsRawFn: func(url string, settings *rawSharedLinkSettings, removeExpiration bool) error {
t.Fatalf("ModifySharedLinkSettingsRaw called during dry-run: %s %#v removeExpiration=%t", url, settings, removeExpiration)
return nil
},
getSharedLinkMetadataFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, error) {
t.Fatalf("GetSharedLinkMetadata called during dry-run: %v", arg)
return nil, nil
},
})

var stdout bytes.Buffer
cmd := newShareLinkUpdateTestCommand(&stdout, nil)
setShareLinkOutputJSON(t, cmd)
if err := cmd.Flags().Set("audience", "team"); err != nil {
t.Fatalf("set audience: %v", err)
}
if err := cmd.Flags().Set(dryRunFlagName, "true"); err != nil {
t.Fatal(err)
}

if err := shareLinkUpdate(cmd, []string{"https://example.com/report"}); err != nil {
t.Fatalf("shareLinkUpdate error: %v", err)
}

var got struct {
Input shareLinkUpdateInput `json:"input"`
Results []struct {
Status string `json:"status"`
Kind string `json:"kind"`
Input shareLinkUpdateResultInput `json:"input"`
Result shareLinkJSONMetadata `json:"result"`
} `json:"results"`
}
if err := json.Unmarshal(stdout.Bytes(), &got); err != nil {
t.Fatalf("decode output: %v\n%s", err, stdout.String())
}
if got.Input.URL != "https://example.com/report" || got.Input.Audience != "team" || !got.Input.DryRun {
t.Fatalf("input = %+v, want team dry-run update", got.Input)
}
if len(got.Results) != 1 {
t.Fatalf("results len = %d, want 1", len(got.Results))
}
result := got.Results[0]
if result.Status != jsonStatusPlanned || result.Kind != shareLinkJSONKindLink || !result.Input.DryRun {
t.Fatalf("result = %+v, want planned link update", result)
}
if result.Result.Type != shareLinkJSONKindLink || result.Result.URL != "https://example.com/report" {
t.Fatalf("result metadata = %+v, want planned link URL", result.Result)
}
}

func TestShareLinkRevokeJSONOutputsRevokedURL(t *testing.T) {
stubSharedLinkClient(t, &mockSharedLinkClient{
revokeSharedLinkFn: func(arg *sharing.RevokeSharedLinkArg) error {
Expand Down
46 changes: 45 additions & 1 deletion cmd/share_link_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package cmd

import (
"errors"
"io"
"time"

"github.com/dropbox/dbxcli/v3/internal/output"
Expand All @@ -32,6 +33,7 @@ type shareLinkUpdateOptions struct {
audience *sharing.LinkAudience
password sharedLinkPasswordOptions
removePassword bool
dryRun bool
}

type shareLinkUpdateInput struct {
Expand All @@ -43,6 +45,11 @@ type shareLinkUpdateInput struct {
DisallowDownload bool `json:"disallow_download,omitempty"`
Password bool `json:"password,omitempty"`
RemovePassword bool `json:"remove_password,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
}

type shareLinkUpdateResultInput struct {
DryRun bool `json:"dry_run,omitempty"`
}

func shareLinkUpdate(cmd *cobra.Command, args []string) error {
Expand All @@ -60,6 +67,10 @@ func shareLinkUpdate(cmd *cobra.Command, args []string) error {
return err
}

if opts.dryRun {
return renderShareLinkUpdateDryRunOutput(cmd, url, opts)
}

dbx := newSharedLinkClient(config)
if opts.usesRawSettings() {
if err := dbx.ModifySharedLinkSettingsRawContext(currentContext(), url, rawSharedLinkSettingsFromUpdateOptions(opts), opts.removeExpiration); err != nil {
Expand Down Expand Up @@ -125,7 +136,7 @@ func renderShareLinkUpdateOutput(cmd *cobra.Command, dbx sharedLinkClient, url s
return renderJSONOperationOutput(
cmd,
newShareLinkUpdateInput(url, opts),
[]jsonOperationResult{shareLinkJSONOperationResult(shareLinkJSONStatusUpdated, result)},
[]jsonOperationResult{shareLinkUpdateOperationResult(shareLinkJSONStatusUpdated, result, opts)},
)
}

Expand All @@ -137,6 +148,7 @@ func newShareLinkUpdateInput(url string, opts shareLinkUpdateOptions) shareLinkU
DisallowDownload: opts.disallowDownload,
Password: opts.password.set,
RemovePassword: opts.removePassword,
DryRun: opts.dryRun,
}
if opts.audience != nil {
input.Audience = opts.audience.Tag
Expand Down Expand Up @@ -170,6 +182,13 @@ func parseShareLinkUpdateOptions(cmd *cobra.Command) (shareLinkUpdateOptions, er
if err != nil {
return shareLinkUpdateOptions{}, err
}
var dryRun bool
if cmd.Flags().Lookup(dryRunFlagName) != nil {
dryRun, err = dryRunEnabled(cmd)
if err != nil {
return shareLinkUpdateOptions{}, err
}
}

if expiresChanged && removeExpiration {
return shareLinkUpdateOptions{}, invalidArgumentsErrorWithDetails("`--expires` and `--remove-expiration` cannot be used together", flagsErrorDetails("expires", "remove-expiration"))
Expand Down Expand Up @@ -214,6 +233,7 @@ func parseShareLinkUpdateOptions(cmd *cobra.Command) (shareLinkUpdateOptions, er
audience: audience,
password: password,
removePassword: removePassword,
dryRun: dryRun,
}, nil
}

Expand Down Expand Up @@ -246,6 +266,29 @@ func rawSharedLinkSettingsFromUpdateOptions(opts shareLinkUpdateOptions) *rawSha
return settings
}

func shareLinkUpdateOperationResult(status string, metadata shareLinkJSONMetadata, opts shareLinkUpdateOptions) jsonOperationResult {
return newJSONOperationResult(plannedStatus(opts.dryRun, status), metadata.Type, shareLinkUpdateResultInput{DryRun: opts.dryRun}, metadata)
}

func plannedShareLinkUpdateMetadata(url string) shareLinkJSONMetadata {
return shareLinkJSONMetadata{
Type: shareLinkJSONKindLink,
URL: url,
}
}

func renderShareLinkUpdateDryRunOutput(cmd *cobra.Command, url string, opts shareLinkUpdateOptions) error {
result := plannedShareLinkUpdateMetadata(url)
return commandOutput(cmd).Render(func(w io.Writer) error {
return writeDryRunLine(w, "update shared link", url)
}, newJSONCommandOperationOutput(
cmd,
newShareLinkUpdateInput(url, opts),
[]jsonOperationResult{shareLinkUpdateOperationResult(shareLinkJSONStatusUpdated, result, opts)},
nil,
))
}

var shareLinkUpdateCmd = &cobra.Command{
Use: "update <url>",
Short: "Update shared link settings",
Expand All @@ -267,6 +310,7 @@ func init() {
shareLinkUpdateCmd.Flags().Bool("disallow-download", false, "Disallow downloads from the shared link")
addSharedLinkPasswordFlags(shareLinkUpdateCmd)
shareLinkUpdateCmd.Flags().Bool("remove-password", false, "Remove the shared link password")
addDryRunFlag(shareLinkUpdateCmd)
shareLinkCmd.AddCommand(shareLinkUpdateCmd)
enableStructuredOutput(shareLinkUpdateCmd)
}
39 changes: 39 additions & 0 deletions cmd/share_link_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,41 @@ func TestShareLinkUpdateDisallowsDownload(t *testing.T) {
}
}

func TestShareLinkUpdateDryRunDoesNotCallAPI(t *testing.T) {
stubSharedLinkClient(t, &mockSharedLinkClient{
modifySharedLinkSettingsFn: func(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error) {
t.Fatalf("ModifySharedLinkSettings called during dry-run: %v", arg)
return nil, nil
},
modifySharedLinkSettingsRawFn: func(url string, settings *rawSharedLinkSettings, removeExpiration bool) error {
t.Fatalf("ModifySharedLinkSettingsRaw called during dry-run: %s %#v removeExpiration=%t", url, settings, removeExpiration)
return nil
},
getSharedLinkMetadataFn: func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, error) {
t.Fatalf("GetSharedLinkMetadata called during dry-run: %v", arg)
return nil, nil
},
})

var stdout bytes.Buffer
cmd := newShareLinkUpdateTestCommand(&stdout, nil)
if err := cmd.Flags().Set("disallow-download", "true"); err != nil {
t.Fatalf("set disallow-download: %v", err)
}
if err := cmd.Flags().Set(dryRunFlagName, "true"); err != nil {
t.Fatal(err)
}

if err := shareLinkUpdate(cmd, []string{"https://example.com/link"}); err != nil {
t.Fatalf("shareLinkUpdate error: %v", err)
}

const want = "Would update shared link https://example.com/link\n"
if got := stdout.String(); got != want {
t.Fatalf("stdout = %q, want %q", got, want)
}
}

func TestShareLinkUpdateDisallowDownloadCombinesSettingsInOneRawCall(t *testing.T) {
expires := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
var rawCalls int
Expand Down Expand Up @@ -651,6 +686,9 @@ func TestShareLinkUpdateCommandIsRegistered(t *testing.T) {
if shareLinkUpdateCmd.Flags().Lookup("disallow-download") == nil {
t.Fatal("share-link update should define --disallow-download")
}
if shareLinkUpdateCmd.Flags().Lookup(dryRunFlagName) == nil {
t.Fatalf("share-link update should define --%s", dryRunFlagName)
}
}

func newShareLinkUpdateTestCommand(stdout, stderr *bytes.Buffer) *cobra.Command {
Expand All @@ -662,6 +700,7 @@ func newShareLinkUpdateTestCommand(stdout, stderr *bytes.Buffer) *cobra.Command
cmd.Flags().Bool("disallow-download", false, "")
addSharedLinkPasswordFlags(cmd)
cmd.Flags().Bool("remove-password", false, "")
addDryRunFlag(cmd)
cmd.Flags().Bool("verbose", false, "")
if stdout != nil {
cmd.SetOut(stdout)
Expand Down
7 changes: 6 additions & 1 deletion cmd/testdata/json_contract/success_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,12 +338,16 @@
"allow_download",
"audience",
"disallow_download",
"dry_run",
"expires",
"password",
"remove_expiration",
"remove_password",
"url"
],
"share_link_update_result_input": [
"dry_run"
],
"team_group": [
"group_external_id",
"group_id",
Expand Down Expand Up @@ -755,9 +759,10 @@
"top_level": "operation_output",
"result_wrapper": "operation_result",
"input": "share_link_update_input",
"result_input": "empty",
"result_input": "share_link_update_result_input",
"result": "share_link_metadata",
"statuses": [
"planned",
"updated"
],
"kinds": [
Expand Down
3 changes: 2 additions & 1 deletion docs/commands/dbxcli_share-link_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dbxcli share-link update <url> [flags]
--allow-download Allow downloads from the shared link
--audience string Set shared link audience: public, team, members, or no-one
--disallow-download Disallow downloads from the shared link
--dry-run Preview intended writes without making changes
--expires string Set shared link expiration time as an RFC3339 timestamp
-h, --help help for update
--password string Password for password-protected shared links
Expand Down Expand Up @@ -56,7 +57,7 @@ dbxcli share-link update <url> [flags]
* Dropbox scopes: `sharing.read`, `sharing.write`
* Arguments: `url` (required, url)
* Flag metadata: `--allow-download` (conflicts: `disallow-download`), `--audience` (values: `members`, `no-one`, `public`, `team`), `--disallow-download` (conflicts: `allow-download`), `--expires` (conflicts: `remove-expiration`), `--output` (values: `json`, `text`), `--password` (conflicts: `password-file`, `password-prompt`, `remove-password`; sensitive), `--password-file` (conflicts: `password`, `password-prompt`, `remove-password`), `--password-prompt` (conflicts: `password`, `password-file`, `remove-password`; may prompt), `--remove-expiration` (conflicts: `expires`), `--remove-password` (conflicts: `password`, `password-file`, `password-prompt`)
* Result statuses: `updated`
* Result statuses: `planned`, `updated`
* Result kinds: `file`, `folder`, `link`
* JSON contract: `docs/json-schema/v1/commands.json#/commands/share-link update`
* JSON success schema: `docs/json-schema/v1/commands.schema.json#/$defs/command_share_2dlink_20update`
Expand Down
7 changes: 6 additions & 1 deletion docs/json-schema/v1/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,12 +338,16 @@
"allow_download",
"audience",
"disallow_download",
"dry_run",
"expires",
"password",
"remove_expiration",
"remove_password",
"url"
],
"share_link_update_result_input": [
"dry_run"
],
"team_group": [
"group_external_id",
"group_id",
Expand Down Expand Up @@ -755,9 +759,10 @@
"top_level": "operation_output",
"result_wrapper": "operation_result",
"input": "share_link_update_input",
"result_input": "empty",
"result_input": "share_link_update_result_input",
"result": "share_link_metadata",
"statuses": [
"planned",
"updated"
],
"kinds": [
Expand Down
Loading
Loading