cloud/storage/dropbox: add storage.Lister implementation and test

Change-Id: Iedb02eeae08c0d290d60734a80bfc09d7600836d
Reviewed-on: https://upspin-review.googlesource.com/18660
Reviewed-by: Andrew Gerrand <adg@golang.org>
diff --git a/cloud/storage/dropbox/dropbox.go b/cloud/storage/dropbox/dropbox.go
index f464158..de72677 100644
--- a/cloud/storage/dropbox/dropbox.go
+++ b/cloud/storage/dropbox/dropbox.go
@@ -47,8 +47,13 @@
 	token  string
 }
 
-// Guarantee we implement the Storage interface
-var _ storage.Storage = (*dropboxImpl)(nil)
+var (
+	// Guarantee we implement the Storage interface
+	_ storage.Storage = (*dropboxImpl)(nil)
+
+	// Guarantee we implement the storage.Lister interface.
+	_ storage.Lister = (*dropboxImpl)(nil)
+)
 
 // LinkBase implements Storage.
 func (d *dropboxImpl) LinkBase() (base string, err error) {
@@ -137,6 +142,69 @@
 	return nil
 }
 
+// maxResults specifies the number of references to return from each call to
+// List. It is a variable here so that it may be overridden in tests.
+var maxResults int32 = 1000
+
+// List implements storage.Lister.
+func (d *dropboxImpl) List(token string) (refs []upspin.ListRefsItem, nextToken string, err error) {
+	const op errors.Op = "cloud/storage/dropbox.List"
+
+	u := "https://api.dropboxapi.com/2/files/list_folder"
+	arg, _ := json.Marshal(struct {
+		Path  string `json:"path"`
+		Limit int32  `json:"limit"`
+	}{
+		"",
+		maxResults,
+	})
+
+	if token != "" {
+		u = "https://api.dropboxapi.com/2/files/list_folder/continue"
+		arg, _ = json.Marshal(struct {
+			Cursor string `json:"cursor"`
+		}{token})
+	}
+
+	req, err := d.newRequest(u, bytes.NewReader(arg), "")
+	if err != nil {
+		return nil, "", errors.E(op, err)
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	body, err := d.doRequest(req)
+	if err != nil {
+		return nil, "", errors.E(op, err)
+	}
+
+	var objs struct {
+		Items []struct {
+			Name string
+			Size int64
+		} `json:"entries"`
+		NextPageToken string `json:"cursor"`
+		More          bool   `json:"has_more"`
+	}
+
+	err = json.Unmarshal(body, &objs)
+	if err != nil {
+		return nil, "", errors.E(op, err)
+	}
+
+	for _, item := range objs.Items {
+		refs = append(refs, upspin.ListRefsItem{
+			Ref:  upspin.Reference(item.Name),
+			Size: item.Size,
+		})
+	}
+
+	if objs.More {
+		nextToken = objs.NextPageToken
+	}
+
+	return refs, nextToken, nil
+}
+
 // Close implements Storage.
 func (d *dropboxImpl) Close() {
 	// not yet implemented
@@ -181,7 +249,7 @@
 	}
 
 	if resp.StatusCode != 200 {
-		return nil, errors.Errorf(resp.Status)
+		return nil, errors.Errorf("Dropbox API: %s, %s", resp.Status, body)
 	}
 
 	return body, nil
diff --git a/cloud/storage/dropbox/dropbox_test.go b/cloud/storage/dropbox/dropbox_test.go
index 495a424..948b9d8 100644
--- a/cloud/storage/dropbox/dropbox_test.go
+++ b/cloud/storage/dropbox/dropbox_test.go
@@ -17,6 +17,7 @@
 	"golang.org/x/oauth2"
 
 	"upspin.io/cloud/storage"
+	"upspin.io/upspin"
 )
 
 var (
@@ -29,6 +30,67 @@
 	useDropbox = flag.Bool("use_dropbox", false, "enable to run dropbox tests; requires authentication code")
 )
 
+func TestList(t *testing.T) {
+	ls, ok := client.(storage.Lister)
+	if !ok {
+		t.Fatal("impl does not provide List method")
+	}
+
+	refs, next, err := ls.List("")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(refs) != 0 {
+		t.Errorf("list returned %d refs, want 0", len(refs))
+	}
+	if next != "" {
+		t.Errorf("list returned page token %q, want empty", next)
+	}
+
+	// Test pagination by reducing the results per page to 2.
+	oldMaxResults := maxResults
+	defer func() { maxResults = oldMaxResults }()
+	maxResults = 2
+
+	const nFiles = 6 // Must be evenly divisible by maxResults.
+	for i := 0; i < nFiles; i++ {
+		fn := fmt.Sprintf("test-%d", i)
+		err = client.Put(fn, testData)
+		if err != nil {
+			t.Fatal(err)
+		}
+		// clean up
+		defer client.Delete(fn)
+	}
+
+	seen := make(map[upspin.Reference]bool)
+	for i := 0; i < nFiles/2; i++ {
+		refs, next, err = ls.List(next)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if len(refs) != 2 {
+			t.Errorf("got %d refs, want 2", len(refs))
+		}
+		if i == nFiles/2-1 {
+			if next != "" {
+				t.Errorf("got page token %q, want empty", next)
+			}
+		} else if next == "" {
+			t.Error("got empty page token, want non-empty")
+		}
+		for _, ref := range refs {
+			if seen[ref.Ref] {
+				t.Errorf("saw duplicate ref %q", ref.Ref)
+			}
+			seen[ref.Ref] = true
+			if got, want := ref.Size, int64(len(testData)); got != want {
+				t.Errorf("ref %q has size %d, want %d", ref.Ref, got, want)
+			}
+		}
+	}
+}
+
 // This is more of a regression test as it uses the running cloud
 // storage in prod. However, since Dropbox is always available, we accept
 // to rely on it.