github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/internal/codespaces/api/api_test.go (about) 1 package api 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "net/http/httptest" 9 "reflect" 10 "strconv" 11 "testing" 12 ) 13 14 func generateCodespaceList(start int, end int) []*Codespace { 15 codespacesList := []*Codespace{} 16 for i := start; i < end; i++ { 17 codespacesList = append(codespacesList, &Codespace{ 18 Name: fmt.Sprintf("codespace-%d", i), 19 }) 20 } 21 return codespacesList 22 } 23 24 func createFakeListEndpointServer(t *testing.T, initalTotal int, finalTotal int) *httptest.Server { 25 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 if r.URL.Path != "/user/codespaces" { 27 t.Fatal("Incorrect path") 28 } 29 30 page := 1 31 if r.URL.Query().Get("page") != "" { 32 page, _ = strconv.Atoi(r.URL.Query().Get("page")) 33 } 34 35 per_page := 0 36 if r.URL.Query().Get("per_page") != "" { 37 per_page, _ = strconv.Atoi(r.URL.Query().Get("per_page")) 38 } 39 40 response := struct { 41 Codespaces []*Codespace `json:"codespaces"` 42 TotalCount int `json:"total_count"` 43 }{ 44 Codespaces: []*Codespace{}, 45 TotalCount: finalTotal, 46 } 47 48 switch page { 49 case 1: 50 response.Codespaces = generateCodespaceList(0, per_page) 51 response.TotalCount = initalTotal 52 w.Header().Set("Link", fmt.Sprintf(`<http://%[1]s/user/codespaces?page=3&per_page=%[2]d>; rel="last", <http://%[1]s/user/codespaces?page=2&per_page=%[2]d>; rel="next"`, r.Host, per_page)) 53 case 2: 54 response.Codespaces = generateCodespaceList(per_page, per_page*2) 55 response.TotalCount = finalTotal 56 w.Header().Set("Link", fmt.Sprintf(`<http://%s/user/codespaces?page=3&per_page=%d>; rel="next"`, r.Host, per_page)) 57 case 3: 58 response.Codespaces = generateCodespaceList(per_page*2, per_page*3-per_page/2) 59 response.TotalCount = finalTotal 60 default: 61 t.Fatal("Should not check extra page") 62 } 63 64 data, _ := json.Marshal(response) 65 fmt.Fprint(w, string(data)) 66 })) 67 } 68 69 func createFakeCreateEndpointServer(t *testing.T, wantStatus int) *httptest.Server { 70 t.Helper() 71 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 72 // create endpoint 73 if r.URL.Path == "/user/codespaces" { 74 body := r.Body 75 if body == nil { 76 t.Fatal("No body") 77 } 78 defer body.Close() 79 80 var params startCreateRequest 81 err := json.NewDecoder(body).Decode(¶ms) 82 if err != nil { 83 t.Fatal("error:", err) 84 } 85 86 if params.RepositoryID != 1 { 87 t.Fatal("Expected RepositoryID to be 1. Got: ", params.RepositoryID) 88 } 89 90 if params.IdleTimeoutMinutes != 10 { 91 t.Fatal("Expected IdleTimeoutMinutes to be 10. Got: ", params.IdleTimeoutMinutes) 92 } 93 94 if *params.RetentionPeriodMinutes != 0 { 95 t.Fatal("Expected RetentionPeriodMinutes to be 0. Got: ", *params.RetentionPeriodMinutes) 96 } 97 98 response := Codespace{ 99 Name: "codespace-1", 100 } 101 102 if wantStatus == 0 { 103 wantStatus = http.StatusCreated 104 } 105 106 data, _ := json.Marshal(response) 107 w.WriteHeader(wantStatus) 108 fmt.Fprint(w, string(data)) 109 return 110 } 111 112 // get endpoint hit for testing pending status 113 if r.URL.Path == "/user/codespaces/codespace-1" { 114 response := Codespace{ 115 Name: "codespace-1", 116 State: CodespaceStateAvailable, 117 } 118 data, _ := json.Marshal(response) 119 w.WriteHeader(http.StatusOK) 120 fmt.Fprint(w, string(data)) 121 return 122 } 123 124 t.Fatal("Incorrect path") 125 })) 126 } 127 128 func TestCreateCodespaces(t *testing.T) { 129 svr := createFakeCreateEndpointServer(t, http.StatusCreated) 130 defer svr.Close() 131 132 api := API{ 133 githubAPI: svr.URL, 134 client: &http.Client{}, 135 } 136 137 ctx := context.TODO() 138 retentionPeriod := 0 139 params := &CreateCodespaceParams{ 140 RepositoryID: 1, 141 IdleTimeoutMinutes: 10, 142 RetentionPeriodMinutes: &retentionPeriod, 143 } 144 codespace, err := api.CreateCodespace(ctx, params) 145 if err != nil { 146 t.Fatal(err) 147 } 148 149 if codespace.Name != "codespace-1" { 150 t.Fatalf("expected codespace-1, got %s", codespace.Name) 151 } 152 } 153 154 func TestCreateCodespaces_Pending(t *testing.T) { 155 svr := createFakeCreateEndpointServer(t, http.StatusAccepted) 156 defer svr.Close() 157 158 api := API{ 159 githubAPI: svr.URL, 160 client: &http.Client{}, 161 } 162 163 ctx := context.TODO() 164 retentionPeriod := 0 165 params := &CreateCodespaceParams{ 166 RepositoryID: 1, 167 IdleTimeoutMinutes: 10, 168 RetentionPeriodMinutes: &retentionPeriod, 169 } 170 codespace, err := api.CreateCodespace(ctx, params) 171 if err != nil { 172 t.Fatal(err) 173 } 174 175 if codespace.Name != "codespace-1" { 176 t.Fatalf("expected codespace-1, got %s", codespace.Name) 177 } 178 } 179 180 func TestListCodespaces_limited(t *testing.T) { 181 svr := createFakeListEndpointServer(t, 200, 200) 182 defer svr.Close() 183 184 api := API{ 185 githubAPI: svr.URL, 186 client: &http.Client{}, 187 } 188 ctx := context.TODO() 189 codespaces, err := api.ListCodespaces(ctx, ListCodespacesOptions{Limit: 200}) 190 if err != nil { 191 t.Fatal(err) 192 } 193 194 if len(codespaces) != 200 { 195 t.Fatalf("expected 200 codespace, got %d", len(codespaces)) 196 } 197 if codespaces[0].Name != "codespace-0" { 198 t.Fatalf("expected codespace-0, got %s", codespaces[0].Name) 199 } 200 if codespaces[199].Name != "codespace-199" { 201 t.Fatalf("expected codespace-199, got %s", codespaces[0].Name) 202 } 203 } 204 205 func TestListCodespaces_unlimited(t *testing.T) { 206 svr := createFakeListEndpointServer(t, 200, 200) 207 defer svr.Close() 208 209 api := API{ 210 githubAPI: svr.URL, 211 client: &http.Client{}, 212 } 213 ctx := context.TODO() 214 codespaces, err := api.ListCodespaces(ctx, ListCodespacesOptions{}) 215 if err != nil { 216 t.Fatal(err) 217 } 218 219 if len(codespaces) != 250 { 220 t.Fatalf("expected 250 codespace, got %d", len(codespaces)) 221 } 222 if codespaces[0].Name != "codespace-0" { 223 t.Fatalf("expected codespace-0, got %s", codespaces[0].Name) 224 } 225 if codespaces[249].Name != "codespace-249" { 226 t.Fatalf("expected codespace-249, got %s", codespaces[0].Name) 227 } 228 } 229 230 func TestGetRepoSuggestions(t *testing.T) { 231 tests := []struct { 232 searchText string // The input search string 233 queryText string // The wanted query string (based off searchText) 234 sort string // (Optional) The RepoSearchParameters.Sort param 235 maxRepos string // (Optional) The RepoSearchParameters.MaxRepos param 236 }{ 237 { 238 searchText: "test", 239 queryText: "test", 240 }, 241 { 242 searchText: "org/repo", 243 queryText: "repo user:org", 244 }, 245 { 246 searchText: "org/repo/extra", 247 queryText: "repo/extra user:org", 248 }, 249 { 250 searchText: "test", 251 queryText: "test", 252 sort: "stars", 253 maxRepos: "1000", 254 }, 255 } 256 257 for _, tt := range tests { 258 runRepoSearchTest(t, tt.searchText, tt.queryText, tt.sort, tt.maxRepos) 259 } 260 } 261 262 func createFakeSearchReposServer(t *testing.T, wantSearchText string, wantSort string, wantPerPage string, responseRepos []*Repository) *httptest.Server { 263 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 264 if r.URL.Path != "/search/repositories" { 265 t.Error("Incorrect path") 266 return 267 } 268 269 query := r.URL.Query() 270 got := fmt.Sprintf("q=%q sort=%s per_page=%s", query.Get("q"), query.Get("sort"), query.Get("per_page")) 271 want := fmt.Sprintf("q=%q sort=%s per_page=%s", wantSearchText+" in:name", wantSort, wantPerPage) 272 if got != want { 273 t.Errorf("for query, got %s, want %s", got, want) 274 return 275 } 276 277 response := struct { 278 Items []*Repository `json:"items"` 279 }{ 280 responseRepos, 281 } 282 283 if err := json.NewEncoder(w).Encode(response); err != nil { 284 t.Error(err) 285 } 286 })) 287 } 288 289 func runRepoSearchTest(t *testing.T, searchText, wantQueryText, wantSort, wantMaxRepos string) { 290 wantRepoNames := []string{"repo1", "repo2"} 291 292 apiResponseRepositories := make([]*Repository, 0) 293 for _, name := range wantRepoNames { 294 apiResponseRepositories = append(apiResponseRepositories, &Repository{FullName: name}) 295 } 296 297 svr := createFakeSearchReposServer(t, wantQueryText, wantSort, wantMaxRepos, apiResponseRepositories) 298 defer svr.Close() 299 300 api := API{ 301 githubAPI: svr.URL, 302 client: &http.Client{}, 303 } 304 305 ctx := context.Background() 306 307 searchParameters := RepoSearchParameters{} 308 if len(wantSort) > 0 { 309 searchParameters.Sort = wantSort 310 } 311 if len(wantMaxRepos) > 0 { 312 searchParameters.MaxRepos, _ = strconv.Atoi(wantMaxRepos) 313 } 314 315 gotRepoNames, err := api.GetCodespaceRepoSuggestions(ctx, searchText, searchParameters) 316 if err != nil { 317 t.Fatal(err) 318 } 319 320 gotNamesStr := fmt.Sprintf("%v", gotRepoNames) 321 wantNamesStr := fmt.Sprintf("%v", wantRepoNames) 322 if gotNamesStr != wantNamesStr { 323 t.Fatalf("got repo names %s, want %s", gotNamesStr, wantNamesStr) 324 } 325 } 326 327 func TestRetries(t *testing.T) { 328 var callCount int 329 csName := "test_codespace" 330 handler := func(w http.ResponseWriter, r *http.Request) { 331 if callCount == 3 { 332 err := json.NewEncoder(w).Encode(Codespace{ 333 Name: csName, 334 }) 335 if err != nil { 336 t.Fatal(err) 337 } 338 return 339 } 340 callCount++ 341 w.WriteHeader(502) 342 } 343 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler(w, r) })) 344 t.Cleanup(srv.Close) 345 a := &API{ 346 githubAPI: srv.URL, 347 client: &http.Client{}, 348 } 349 cs, err := a.GetCodespace(context.Background(), "test", false) 350 if err != nil { 351 t.Fatal(err) 352 } 353 if callCount != 3 { 354 t.Fatalf("expected at least 2 retries but got %d", callCount) 355 } 356 if cs.Name != csName { 357 t.Fatalf("expected codespace name to be %q but got %q", csName, cs.Name) 358 } 359 callCount = 0 360 handler = func(w http.ResponseWriter, r *http.Request) { 361 callCount++ 362 err := json.NewEncoder(w).Encode(Codespace{ 363 Name: csName, 364 }) 365 if err != nil { 366 t.Fatal(err) 367 } 368 } 369 cs, err = a.GetCodespace(context.Background(), "test", false) 370 if err != nil { 371 t.Fatal(err) 372 } 373 if callCount != 1 { 374 t.Fatalf("expected no retries but got %d calls", callCount) 375 } 376 if cs.Name != csName { 377 t.Fatalf("expected codespace name to be %q but got %q", csName, cs.Name) 378 } 379 } 380 381 func TestCodespace_ExportData(t *testing.T) { 382 type fields struct { 383 Name string 384 CreatedAt string 385 DisplayName string 386 LastUsedAt string 387 Owner User 388 Repository Repository 389 State string 390 GitStatus CodespaceGitStatus 391 Connection CodespaceConnection 392 Machine CodespaceMachine 393 } 394 type args struct { 395 fields []string 396 } 397 tests := []struct { 398 name string 399 fields fields 400 args args 401 want map[string]interface{} 402 }{ 403 { 404 name: "just name", 405 fields: fields{ 406 Name: "test", 407 }, 408 args: args{ 409 fields: []string{"name"}, 410 }, 411 want: map[string]interface{}{ 412 "name": "test", 413 }, 414 }, 415 { 416 name: "just owner", 417 fields: fields{ 418 Owner: User{ 419 Login: "test", 420 }, 421 }, 422 args: args{ 423 fields: []string{"owner"}, 424 }, 425 want: map[string]interface{}{ 426 "owner": "test", 427 }, 428 }, 429 { 430 name: "just machine", 431 fields: fields{ 432 Machine: CodespaceMachine{ 433 Name: "test", 434 }, 435 }, 436 args: args{ 437 fields: []string{"machineName"}, 438 }, 439 want: map[string]interface{}{ 440 "machineName": "test", 441 }, 442 }, 443 } 444 for _, tt := range tests { 445 t.Run(tt.name, func(t *testing.T) { 446 c := &Codespace{ 447 Name: tt.fields.Name, 448 CreatedAt: tt.fields.CreatedAt, 449 DisplayName: tt.fields.DisplayName, 450 LastUsedAt: tt.fields.LastUsedAt, 451 Owner: tt.fields.Owner, 452 Repository: tt.fields.Repository, 453 State: tt.fields.State, 454 GitStatus: tt.fields.GitStatus, 455 Connection: tt.fields.Connection, 456 Machine: tt.fields.Machine, 457 } 458 if got := c.ExportData(tt.args.fields); !reflect.DeepEqual(got, tt.want) { 459 t.Errorf("Codespace.ExportData() = %v, want %v", got, tt.want) 460 } 461 }) 462 } 463 } 464 465 func createFakeEditServer(t *testing.T, codespaceName string) *httptest.Server { 466 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 467 checkPath := "/user/codespaces/" + codespaceName 468 469 if r.URL.Path != checkPath { 470 t.Fatal("Incorrect path") 471 } 472 473 if r.Method != http.MethodPatch { 474 t.Fatal("Incorrect method") 475 } 476 477 body := r.Body 478 if body == nil { 479 t.Fatal("No body") 480 } 481 defer body.Close() 482 483 var data map[string]interface{} 484 err := json.NewDecoder(body).Decode(&data) 485 486 if err != nil { 487 t.Fatal(err) 488 } 489 490 if data["display_name"] != "changeTo" { 491 t.Fatal("Incorrect display name") 492 } 493 494 response := Codespace{ 495 DisplayName: "changeTo", 496 } 497 498 responseData, _ := json.Marshal(response) 499 fmt.Fprint(w, string(responseData)) 500 })) 501 } 502 503 func TestAPI_EditCodespace(t *testing.T) { 504 type args struct { 505 ctx context.Context 506 codespaceName string 507 params *EditCodespaceParams 508 } 509 tests := []struct { 510 name string 511 args args 512 want *Codespace 513 wantErr bool 514 }{ 515 { 516 name: "success", 517 args: args{ 518 ctx: context.Background(), 519 codespaceName: "test", 520 params: &EditCodespaceParams{ 521 DisplayName: "changeTo", 522 }, 523 }, 524 want: &Codespace{ 525 DisplayName: "changeTo", 526 }, 527 }, 528 } 529 for _, tt := range tests { 530 t.Run(tt.name, func(t *testing.T) { 531 svr := createFakeEditServer(t, tt.args.codespaceName) 532 defer svr.Close() 533 534 a := &API{ 535 client: &http.Client{}, 536 githubAPI: svr.URL, 537 } 538 got, err := a.EditCodespace(tt.args.ctx, tt.args.codespaceName, tt.args.params) 539 if (err != nil) != tt.wantErr { 540 t.Errorf("API.EditCodespace() error = %v, wantErr %v", err, tt.wantErr) 541 return 542 } 543 if !reflect.DeepEqual(got, tt.want) { 544 t.Errorf("API.EditCodespace() = %v, want %v", got.DisplayName, tt.want.DisplayName) 545 } 546 }) 547 } 548 } 549 550 func createFakeEditPendingOpServer(t *testing.T) *httptest.Server { 551 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 552 if r.Method == http.MethodPatch { 553 w.WriteHeader(http.StatusUnprocessableEntity) 554 return 555 } 556 557 if r.Method == http.MethodGet { 558 response := Codespace{ 559 PendingOperation: true, 560 PendingOperationDisabledReason: "Some pending operation", 561 } 562 563 responseData, _ := json.Marshal(response) 564 fmt.Fprint(w, string(responseData)) 565 return 566 } 567 })) 568 } 569 570 func TestAPI_EditCodespacePendingOperation(t *testing.T) { 571 svr := createFakeEditPendingOpServer(t) 572 defer svr.Close() 573 574 a := &API{ 575 client: &http.Client{}, 576 githubAPI: svr.URL, 577 } 578 579 _, err := a.EditCodespace(context.Background(), "disabledCodespace", &EditCodespaceParams{DisplayName: "some silly name"}) 580 if err == nil { 581 t.Error("Expected pending operation error, but got nothing") 582 } 583 if err.Error() != "codespace is disabled while it has a pending operation: Some pending operation" { 584 t.Errorf("Expected pending operation error, but got %v", err) 585 } 586 }