Skip to content

Commit d35b0c1

Browse files
Merge pull request #241 from dropbox/bundled-team-app-keys
Bundle team app keys and fix ls error handling
2 parents 9d19c7f + b72ba9b commit d35b0c1

6 files changed

Lines changed: 120 additions & 86 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,19 @@ $ dbxcli login
131131
Commands require saved credentials. If no saved credentials are available, run
132132
`dbxcli login` first or provide a token with `DBXCLI_ACCESS_TOKEN`.
133133

134-
Personal login uses the bundled Dropbox app key by default. You can pass a
135-
custom app key as an option:
134+
Personal and team logins use bundled Dropbox app keys by default. You can pass
135+
a custom app key as an option:
136136

137137
```sh
138138
$ dbxcli login --app-key=your-app-key
139139
```
140140

141-
You can also set it with an environment variable:
141+
You can also set custom app keys with environment variables:
142142

143143
```sh
144144
$ DROPBOX_PERSONAL_APP_KEY=your-app-key dbxcli login
145+
$ DROPBOX_TEAM_APP_KEY=your-app-key dbxcli login team-access
146+
$ DROPBOX_MANAGE_APP_KEY=your-app-key dbxcli login team-manage
145147
```
146148

147149
Saved login credentials include a Dropbox refresh token and are refreshed

cmd/auth.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ func oauthConfigWithAppKey(appKey string, domain string) *oauth2.Config {
108108
}
109109
}
110110

111-
func (t *storedCredential) UnmarshalJSON(b []byte) error {
111+
func (c *storedCredential) UnmarshalJSON(b []byte) error {
112112
var accessToken string
113113
if err := json.Unmarshal(b, &accessToken); err == nil {
114-
*t = storedCredential{AccessToken: accessToken}
114+
*c = storedCredential{AccessToken: accessToken}
115115
return nil
116116
}
117117

@@ -120,7 +120,7 @@ func (t *storedCredential) UnmarshalJSON(b []byte) error {
120120
if err := json.Unmarshal(b, &credential); err != nil {
121121
return err
122122
}
123-
*t = storedCredential(credential)
123+
*c = storedCredential(credential)
124124
return nil
125125
}
126126

@@ -138,7 +138,7 @@ func storedCredentialFromOAuthToken(token *oauth2.Token, appKey string) storedCr
138138
return credential
139139
}
140140

141-
func (c storedCredential) oauthToken() *oauth2.Token {
141+
func (c *storedCredential) oauthToken() *oauth2.Token {
142142
token := &oauth2.Token{
143143
AccessToken: c.AccessToken,
144144
TokenType: c.TokenType,
@@ -150,7 +150,7 @@ func (c storedCredential) oauthToken() *oauth2.Token {
150150
return token
151151
}
152152

153-
func (c storedCredential) shouldRefresh(now time.Time) bool {
153+
func (c *storedCredential) shouldRefresh(now time.Time) bool {
154154
if c.RefreshToken == "" {
155155
return false
156156
}
@@ -253,9 +253,9 @@ func getAccessToken(tokType string, domain string, force bool) (string, string,
253253
func loginCommand(tokType string) string {
254254
switch tokType {
255255
case tokenTeamAccess:
256-
return "dbxcli login team-access --app-key=<your-app-key>"
256+
return "dbxcli login team-access"
257257
case tokenTeamManage:
258-
return "dbxcli login team-manage --app-key=<your-app-key>"
258+
return "dbxcli login team-manage"
259259
default:
260260
return "dbxcli login"
261261
}

cmd/auth_test.go

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -465,8 +465,8 @@ func TestLoginCommandForTokenType(t *testing.T) {
465465
want string
466466
}{
467467
{tokenPersonal, "dbxcli login"},
468-
{tokenTeamAccess, "dbxcli login team-access --app-key=<your-app-key>"},
469-
{tokenTeamManage, "dbxcli login team-manage --app-key=<your-app-key>"},
468+
{tokenTeamAccess, "dbxcli login team-access"},
469+
{tokenTeamManage, "dbxcli login team-manage"},
470470
}
471471

472472
for _, tt := range tests {
@@ -543,7 +543,7 @@ func TestRequestAccessTokenReturnsReadError(t *testing.T) {
543543
}
544544
}
545545

546-
func TestRequestAccessTokenPromptsForAppKeyWhenUsingBundledTeamDefaults(t *testing.T) {
546+
func TestRequestAccessTokenUsesDefaultTeamManageAppKey(t *testing.T) {
547547
restoreOAuthCredentials(t)
548548
setOAuthCredentials(tokenTeamManage, defaultTeamManageAppKey)
549549

@@ -555,17 +555,15 @@ func TestRequestAccessTokenPromptsForAppKeyWhenUsingBundledTeamDefaults(t *testi
555555
})
556556

557557
readAppCredentials = func(tokType string) (appCredentials, error) {
558-
if tokType != tokenTeamManage {
559-
t.Fatalf("expected team manage app credentials prompt, got %q", tokType)
560-
}
561-
return appCredentials{Key: "prompt-key"}, nil
558+
t.Fatal("app credential prompt should not be used for the default team manage app key")
559+
return appCredentials{}, nil
562560
}
563561
readAuthorizationCode = func() (string, error) {
564562
return "auth-code", nil
565563
}
566564
exchangeAuthorizationCode = func(ctx context.Context, conf *oauth2.Config, code string, verifier string) (*oauth2.Token, error) {
567-
if conf.ClientID != "prompt-key" {
568-
t.Fatalf("expected prompted app key, got %q", conf.ClientID)
565+
if conf.ClientID != defaultTeamManageAppKey {
566+
t.Fatalf("expected default team manage app key, got %q", conf.ClientID)
569567
}
570568
if conf.ClientSecret != "" {
571569
t.Fatalf("expected no client secret for PKCE, got %q", conf.ClientSecret)
@@ -580,9 +578,6 @@ func TestRequestAccessTokenPromptsForAppKeyWhenUsingBundledTeamDefaults(t *testi
580578
if token != "access-token" {
581579
t.Fatalf("expected access token, got %q", token)
582580
}
583-
if teamManageAppKey != "prompt-key" {
584-
t.Fatalf("expected prompted app key to be saved for this process, got %q", teamManageAppKey)
585-
}
586581
}
587582

588583
func TestRequestAccessTokenUsesDefaultPersonalAppKey(t *testing.T) {
@@ -724,7 +719,7 @@ func TestRequestAccessTokenUsesConfiguredAppCredentials(t *testing.T) {
724719

725720
func TestRequestAccessTokenRejectsEmptyAppCredentials(t *testing.T) {
726721
restoreOAuthCredentials(t)
727-
setOAuthCredentials(tokenTeamManage, defaultTeamManageAppKey)
722+
setOAuthCredentials(tokenTeamManage, "")
728723

729724
origReadAuthorizationCode := readAuthorizationCode
730725
origExchangeAuthorizationCode := exchangeAuthorizationCode

cmd/ls.go

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package cmd
1616

1717
import (
18+
"errors"
1819
"fmt"
1920
"io"
2021
"os"
@@ -43,12 +44,12 @@ func getFileMetadata(c files.Client, path string) (files.IsMetadata, error) {
4344

4445
// Invoked by search.go
4546
func printFolderMetadata(w io.Writer, e *files.FolderMetadata, longFormat bool) {
46-
fmt.Fprint(w, formatFolderMetadata(e, longFormat))
47+
_, _ = fmt.Fprint(w, formatFolderMetadata(e, longFormat))
4748
}
4849

4950
// Invoked by search.go and revs.go
5051
func printFileMetadata(w io.Writer, e *files.FileMetadata, longFormat bool) {
51-
fmt.Fprint(w, formatFileMetadata(e, longFormat))
52+
_, _ = fmt.Fprint(w, formatFileMetadata(e, longFormat))
5253
}
5354

5455
func formatFolderMetadata(e *files.FolderMetadata, longFormat bool) string {
@@ -80,7 +81,7 @@ func formatDeletedMetadata(e *files.DeletedMetadata, longFormat bool) string {
8081
return text
8182
}
8283

83-
func SetPathDisplayAsDeleted(metadata files.IsMetadata) {
84+
func setPathDisplayAsDeleted(metadata files.IsMetadata) {
8485
switch item := metadata.(type) {
8586
case *files.FileMetadata:
8687
item.PathDisplay = fmt.Sprintf(deletedItemFormatString, item.PathDisplay)
@@ -127,16 +128,16 @@ func ls(cmd *cobra.Command, args []string) (err error) {
127128
itemCounter := 0
128129
printItem := func(message string) {
129130
itemCounter = itemCounter + 1
130-
fmt.Fprint(w, message)
131+
_, _ = fmt.Fprint(w, message)
131132
if (itemCounter%4 == 0) || opts.long {
132-
fmt.Fprintln(w)
133+
_, _ = fmt.Fprintln(w)
133134
}
134135
}
135136

136137
dbx := files.New(config)
137138

138139
if opts.long {
139-
fmt.Fprint(w, "Revision\tSize\tLast modified\tPath\n")
140+
_, _ = fmt.Fprint(w, "Revision\tSize\tLast modified\tPath\n")
140141
}
141142

142143
if path != "" {
@@ -150,8 +151,7 @@ func ls(cmd *cobra.Command, args []string) (err error) {
150151
case *files.FileMetadata:
151152
if !onlyDeleted {
152153
printItem(formatFileMetadataWithOpts(f, opts))
153-
err = w.Flush()
154-
return err
154+
return finishListOutput(w, itemCounter, opts)
155155
}
156156
}
157157
}
@@ -160,22 +160,14 @@ func ls(cmd *cobra.Command, args []string) (err error) {
160160

161161
var entries []files.IsMetadata
162162
if err != nil {
163-
listRevisionError, ok := err.(files.ListRevisionsAPIError)
164-
if ok {
165-
// Don't treat a "not_folder" error as fatal; recover by sending a
166-
// get_metadata request for the same path and using that response instead.
167-
if listRevisionError.EndpointError.Path.Tag == files.LookupErrorNotFolder {
168-
var metaRes files.IsMetadata
169-
metaRes, _ = getFileMetadata(dbx, path)
170-
entries = []files.IsMetadata{metaRes}
171-
} else {
172-
// Return if there's an error other than "not_folder" or if the follow-up
173-
// metadata request fails.
174-
return err
175-
}
176-
} else {
163+
if !isListFolderNotFolderError(err) {
177164
return err
178165
}
166+
// Don't treat a "not_folder" error as fatal; recover by sending a
167+
// get_metadata request for the same path and using that response instead.
168+
var metaRes files.IsMetadata
169+
metaRes, _ = getFileMetadata(dbx, path)
170+
entries = []files.IsMetadata{metaRes}
179171
} else {
180172
entries = res.Entries
181173

@@ -199,26 +191,22 @@ func ls(cmd *cobra.Command, args []string) (err error) {
199191
revisionArg := files.NewListRevisionsArg(deletedItem.PathLower)
200192
res, err := dbx.ListRevisions(revisionArg)
201193
if err != nil {
202-
listRevisionError, ok := err.(files.ListRevisionsAPIError)
203-
if ok {
204-
// We have a ListRevisionsAPIERror
205-
if listRevisionError.EndpointError.Path.Tag == files.LookupErrorNotFile {
206-
// Don't treat a "not_file" error as fatal; recover by sending a
207-
// get_metadata request for the same path and using that response instead.
208-
revision, err := getFileMetadata(dbx, deletedItem.PathLower)
209-
if err != nil {
210-
return err
211-
}
212-
entry = revision
194+
if isListRevisionsNotFileError(err) {
195+
// Don't treat a "not_file" error as fatal; recover by sending a
196+
// get_metadata request for the same path and using that response instead.
197+
revision, err := getFileMetadata(dbx, deletedItem.PathLower)
198+
if err != nil {
199+
return err
213200
}
201+
entry = revision
214202
}
215203
} else if len(res.Entries) == 0 {
216204
// Occasionally revisions will be returned with an empty Revision entry list.
217205
// So we just use the original entry.
218206
} else {
219207
entry = res.Entries[0]
220208
}
221-
SetPathDisplayAsDeleted(entry)
209+
setPathDisplayAsDeleted(entry)
222210
}
223211
switch f := entry.(type) {
224212
case *files.FileMetadata:
@@ -234,8 +222,30 @@ func ls(cmd *cobra.Command, args []string) (err error) {
234222
}
235223
}
236224

237-
err = w.Flush()
238-
return err
225+
return finishListOutput(w, itemCounter, opts)
226+
}
227+
228+
func isListFolderNotFolderError(err error) bool {
229+
var apiErr files.ListFolderAPIError
230+
return errors.As(err, &apiErr) &&
231+
apiErr.EndpointError != nil &&
232+
apiErr.EndpointError.Path != nil &&
233+
apiErr.EndpointError.Path.Tag == files.LookupErrorNotFolder
234+
}
235+
236+
func isListRevisionsNotFileError(err error) bool {
237+
var apiErr files.ListRevisionsAPIError
238+
return errors.As(err, &apiErr) &&
239+
apiErr.EndpointError != nil &&
240+
apiErr.EndpointError.Path != nil &&
241+
apiErr.EndpointError.Path.Tag == files.LookupErrorNotFile
242+
}
243+
244+
func finishListOutput(w *tabwriter.Writer, itemCounter int, opts listOptions) error {
245+
if itemCounter > 0 && !opts.long && itemCounter%4 != 0 {
246+
_, _ = fmt.Fprintln(w)
247+
}
248+
return w.Flush()
239249
}
240250

241251
// lsCmd represents the ls command

cmd/ls_test.go

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package cmd
22

33
import (
4+
"fmt"
5+
"strings"
46
"testing"
7+
"text/tabwriter"
58
"time"
69

710
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox"
@@ -76,23 +79,23 @@ func TestSetPathDisplayAsDeleted(t *testing.T) {
7679
file := &files.FileMetadata{
7780
Metadata: files.Metadata{PathDisplay: "/file.txt"},
7881
}
79-
SetPathDisplayAsDeleted(file)
82+
setPathDisplayAsDeleted(file)
8083
if file.PathDisplay != "<</file.txt>>" {
8184
t.Errorf("file PathDisplay = %q, want %q", file.PathDisplay, "<</file.txt>>")
8285
}
8386

8487
folder := &files.FolderMetadata{
8588
Metadata: files.Metadata{PathDisplay: "/folder"},
8689
}
87-
SetPathDisplayAsDeleted(folder)
90+
setPathDisplayAsDeleted(folder)
8891
if folder.PathDisplay != "<</folder>>" {
8992
t.Errorf("folder PathDisplay = %q, want %q", folder.PathDisplay, "<</folder>>")
9093
}
9194

9295
deleted := &files.DeletedMetadata{
9396
Metadata: files.Metadata{PathDisplay: "/gone"},
9497
}
95-
SetPathDisplayAsDeleted(deleted)
98+
setPathDisplayAsDeleted(deleted)
9699
if deleted.PathDisplay != "<</gone>>" {
97100
t.Errorf("deleted PathDisplay = %q, want %q", deleted.PathDisplay, "<</gone>>")
98101
}
@@ -132,6 +135,51 @@ func TestGetFileMetadataNotCalledForRoot(t *testing.T) {
132135
}
133136
}
134137

138+
func TestFinishListOutputAddsTrailingNewlineForPartialShortRows(t *testing.T) {
139+
var out strings.Builder
140+
w := new(tabwriter.Writer)
141+
w.Init(&out, 4, 8, 1, ' ', 0)
142+
143+
fmt.Fprint(w, "/one\t")
144+
if err := finishListOutput(w, 1, listOptions{}); err != nil {
145+
t.Fatalf("finishListOutput returned error: %v", err)
146+
}
147+
148+
if got := out.String(); !strings.HasSuffix(got, "\n") {
149+
t.Fatalf("output %q does not end with newline", got)
150+
}
151+
}
152+
153+
func TestIsListFolderNotFolderErrorHandlesWrappedErrors(t *testing.T) {
154+
apiErr := files.ListFolderAPIError{
155+
EndpointError: &files.ListFolderError{
156+
Path: &files.LookupError{Tagged: dropbox.Tagged{Tag: files.LookupErrorNotFolder}},
157+
},
158+
}
159+
160+
if !isListFolderNotFolderError(apiErr) {
161+
t.Fatal("expected raw list_folder not_folder error to match")
162+
}
163+
if !isListFolderNotFolderError(fmt.Errorf("wrapped: %w", apiErr)) {
164+
t.Fatal("expected wrapped list_folder not_folder error to match")
165+
}
166+
}
167+
168+
func TestIsListRevisionsNotFileErrorHandlesWrappedErrors(t *testing.T) {
169+
apiErr := files.ListRevisionsAPIError{
170+
EndpointError: &files.ListRevisionsError{
171+
Path: &files.LookupError{Tagged: dropbox.Tagged{Tag: files.LookupErrorNotFile}},
172+
},
173+
}
174+
175+
if !isListRevisionsNotFileError(apiErr) {
176+
t.Fatal("expected raw list_revisions not_file error to match")
177+
}
178+
if !isListRevisionsNotFileError(fmt.Errorf("wrapped: %w", apiErr)) {
179+
t.Fatal("expected wrapped list_revisions not_file error to match")
180+
}
181+
}
182+
135183
func contains(s, substr string) bool {
136184
return len(s) >= len(substr) && (s == substr || len(s) > 0 && stringContains(s, substr))
137185
}

0 commit comments

Comments
 (0)