Skip to content

Commit 3d0d95c

Browse files
authored
Merge pull request cli#4439 from geramirez/geramirez/codespaces-pagination
Adding pagination to list codespaces
2 parents 52e16df + 20ae9d3 commit 3d0d95c

2 files changed

Lines changed: 120 additions & 20 deletions

File tree

internal/codespaces/api/api.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"fmt"
3535
"io/ioutil"
3636
"net/http"
37+
"strconv"
3738
"strings"
3839
"time"
3940

@@ -189,16 +190,43 @@ type CodespaceEnvironmentConnection struct {
189190
HostPublicKeys []string `json:"hostPublicKeys"`
190191
}
191192

193+
// codespacesListResponse is the response body for the `/user/codespaces` endpoint
194+
type getCodespacesListResponse struct {
195+
Codespaces []*Codespace `json:"codespaces"`
196+
TotalCount int `json:"total_count"`
197+
}
198+
192199
// ListCodespaces returns a list of codespaces for the user.
193-
func (a *API) ListCodespaces(ctx context.Context) ([]*Codespace, error) {
200+
// It consumes all pages returned by the API until all codespaces have been fetched.
201+
func (a *API) ListCodespaces(ctx context.Context) (codespaces []*Codespace, err error) {
202+
per_page := 100
203+
for page := 1; ; page++ {
204+
response, err := a.fetchCodespaces(ctx, page, per_page)
205+
if err != nil {
206+
return nil, err
207+
}
208+
codespaces = append(codespaces, response.Codespaces...)
209+
if page*per_page >= response.TotalCount {
210+
break
211+
}
212+
}
213+
214+
return codespaces, nil
215+
}
216+
217+
func (a *API) fetchCodespaces(ctx context.Context, page int, per_page int) (response *getCodespacesListResponse, err error) {
194218
req, err := http.NewRequest(
195219
http.MethodGet, a.githubAPI+"/user/codespaces", nil,
196220
)
197221
if err != nil {
198222
return nil, fmt.Errorf("error creating request: %w", err)
199223
}
200-
201224
a.setHeaders(req)
225+
q := req.URL.Query()
226+
q.Add("page", strconv.Itoa(page))
227+
q.Add("per_page", strconv.Itoa(per_page))
228+
229+
req.URL.RawQuery = q.Encode()
202230
resp, err := a.do(ctx, req, "/user/codespaces")
203231
if err != nil {
204232
return nil, fmt.Errorf("error making request: %w", err)
@@ -214,13 +242,10 @@ func (a *API) ListCodespaces(ctx context.Context) ([]*Codespace, error) {
214242
return nil, jsonErrorResponse(b)
215243
}
216244

217-
var response struct {
218-
Codespaces []*Codespace `json:"codespaces"`
219-
}
220245
if err := json.Unmarshal(b, &response); err != nil {
221246
return nil, fmt.Errorf("error unmarshaling response: %w", err)
222247
}
223-
return response.Codespaces, nil
248+
return response, nil
224249
}
225250

226251
// GetCodespace returns the user codespace based on the provided name.

internal/codespaces/api/api_test.go

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,61 @@ import (
66
"fmt"
77
"net/http"
88
"net/http/httptest"
9+
"strconv"
910
"testing"
1011
)
1112

12-
func TestListCodespaces(t *testing.T) {
13-
codespaces := []*Codespace{
14-
{
15-
Name: "testcodespace",
16-
CreatedAt: "2021-08-09T10:10:24+02:00",
17-
LastUsedAt: "2021-08-09T13:10:24+02:00",
18-
},
19-
}
20-
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
13+
func generateCodespaceList(start int, end int) []*Codespace {
14+
codespacesList := []*Codespace{}
15+
for i := start; i < end; i++ {
16+
codespacesList = append(codespacesList, &Codespace{
17+
Name: fmt.Sprintf("codespace-%d", i),
18+
})
19+
}
20+
return codespacesList
21+
}
22+
23+
func createFakeListEndpointServer(t *testing.T, initalTotal int, finalTotal int) *httptest.Server {
24+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25+
if r.URL.Path != "/user/codespaces" {
26+
t.Fatal("Incorrect path")
27+
}
28+
29+
page := 1
30+
if r.URL.Query().Get("page") != "" {
31+
page, _ = strconv.Atoi(r.URL.Query().Get("page"))
32+
}
33+
34+
per_page := 0
35+
if r.URL.Query().Get("per_page") != "" {
36+
per_page, _ = strconv.Atoi(r.URL.Query().Get("per_page"))
37+
}
38+
2139
response := struct {
2240
Codespaces []*Codespace `json:"codespaces"`
41+
TotalCount int `json:"total_count"`
2342
}{
24-
Codespaces: codespaces,
43+
Codespaces: []*Codespace{},
44+
TotalCount: finalTotal,
45+
}
46+
47+
if page == 1 {
48+
response.Codespaces = generateCodespaceList(0, per_page)
49+
response.TotalCount = initalTotal
50+
} else if page == 2 {
51+
response.Codespaces = generateCodespaceList(per_page, per_page*2)
52+
response.TotalCount = finalTotal
53+
} else {
54+
t.Fatal("Should not check extra page")
2555
}
56+
2657
data, _ := json.Marshal(response)
2758
fmt.Fprint(w, string(data))
2859
}))
60+
}
61+
62+
func TestListCodespaces(t *testing.T) {
63+
svr := createFakeListEndpointServer(t, 200, 200)
2964
defer svr.Close()
3065

3166
api := API{
@@ -38,13 +73,53 @@ func TestListCodespaces(t *testing.T) {
3873
if err != nil {
3974
t.Fatal(err)
4075
}
76+
if len(codespaces) != 200 {
77+
t.Fatalf("expected 100 codespace, got %d", len(codespaces))
78+
}
4179

42-
if len(codespaces) != 1 {
43-
t.Fatalf("expected 1 codespace, got %d", len(codespaces))
80+
if codespaces[0].Name != "codespace-0" {
81+
t.Fatalf("expected codespace-0, got %s", codespaces[0].Name)
4482
}
4583

46-
if codespaces[0].Name != "testcodespace" {
47-
t.Fatalf("expected testcodespace, got %s", codespaces[0].Name)
84+
if codespaces[199].Name != "codespace-199" {
85+
t.Fatalf("expected codespace-199, got %s", codespaces[0].Name)
86+
}
87+
}
88+
89+
func TestMidIterationDeletion(t *testing.T) {
90+
svr := createFakeListEndpointServer(t, 200, 199)
91+
defer svr.Close()
92+
93+
api := API{
94+
githubAPI: svr.URL,
95+
client: &http.Client{},
96+
token: "faketoken",
97+
}
98+
ctx := context.TODO()
99+
codespaces, err := api.ListCodespaces(ctx)
100+
if err != nil {
101+
t.Fatal(err)
102+
}
103+
if len(codespaces) != 200 {
104+
t.Fatalf("expected 200 codespace, got %d", len(codespaces))
48105
}
106+
}
107+
108+
func TestMidIterationAddition(t *testing.T) {
109+
svr := createFakeListEndpointServer(t, 199, 200)
110+
defer svr.Close()
49111

112+
api := API{
113+
githubAPI: svr.URL,
114+
client: &http.Client{},
115+
token: "faketoken",
116+
}
117+
ctx := context.TODO()
118+
codespaces, err := api.ListCodespaces(ctx)
119+
if err != nil {
120+
t.Fatal(err)
121+
}
122+
if len(codespaces) != 200 {
123+
t.Fatalf("expected 200 codespace, got %d", len(codespaces))
124+
}
50125
}

0 commit comments

Comments
 (0)