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(&params)
    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  }