github.com/saucelabs/saucectl@v0.175.1/internal/http/apitester.go (about)

     1  package http
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"time"
    13  
    14  	"github.com/hashicorp/go-retryablehttp"
    15  	"github.com/saucelabs/saucectl/internal/apitest"
    16  	"github.com/saucelabs/saucectl/internal/multipartext"
    17  	"golang.org/x/time/rate"
    18  
    19  	"github.com/saucelabs/saucectl/internal/config"
    20  	"github.com/saucelabs/saucectl/internal/msg"
    21  )
    22  
    23  // Query rate is queryRequestRate per second.
    24  var queryRequestRate = 1
    25  var rateLimitTokenBucket = 10
    26  
    27  // APITester describes an interface to the api-testing rest endpoints.
    28  type APITester struct {
    29  	HTTPClient         *retryablehttp.Client
    30  	URL                string
    31  	Username           string
    32  	AccessKey          string
    33  	RequestRateLimiter *rate.Limiter
    34  }
    35  
    36  // PublishedTest describes a published test.
    37  type PublishedTest struct {
    38  	Published apitest.Test
    39  }
    40  
    41  // VaultErrResponse describes the response when a malformed Vault is unable to be parsed
    42  type VaultErrResponse struct {
    43  	Message struct {
    44  		Errors []vaultErr `json:"errors,omitempty"`
    45  	} `json:"message,omitempty"`
    46  	Status string `json:"status,omitempty"`
    47  }
    48  
    49  // DriveErrResponse describes the response when drive API returns an error.
    50  type DriveErrResponse struct {
    51  	Error   string `json:"error"`
    52  	Message string `json:"message"`
    53  }
    54  
    55  type vaultErr struct {
    56  	Field         string                  `json:"field,omitempty"`
    57  	Message       string                  `json:"message,omitempty"`
    58  	Object        string                  `json:"object,omitempty"`
    59  	RejectedValue []apitest.VaultVariable `json:"rejected-value,omitempty"`
    60  }
    61  
    62  type vaultFileDeletion struct {
    63  	FileNames []string `json:"fileNames"`
    64  }
    65  
    66  // NewAPITester a new instance of APITester.
    67  func NewAPITester(url string, username string, accessKey string, timeout time.Duration) APITester {
    68  	return APITester{
    69  		HTTPClient:         NewRetryableClient(timeout),
    70  		URL:                url,
    71  		Username:           username,
    72  		AccessKey:          accessKey,
    73  		RequestRateLimiter: rate.NewLimiter(rate.Every(time.Duration(1/queryRequestRate)*time.Second), rateLimitTokenBucket),
    74  	}
    75  }
    76  
    77  // GetProject returns Project metadata for a given hookID.
    78  func (c *APITester) GetProject(ctx context.Context, hookID string) (apitest.ProjectMeta, error) {
    79  	url := fmt.Sprintf("%s/api-testing/rest/v4/%s", c.URL, hookID)
    80  	req, err := NewRetryableRequestWithContext(ctx, http.MethodGet, url, nil)
    81  	if err != nil {
    82  		return apitest.ProjectMeta{}, err
    83  	}
    84  
    85  	req.SetBasicAuth(c.Username, c.AccessKey)
    86  	resp, err := c.HTTPClient.Do(req)
    87  	if err != nil {
    88  		return apitest.ProjectMeta{}, err
    89  	}
    90  	defer resp.Body.Close()
    91  
    92  	if resp.StatusCode >= http.StatusInternalServerError {
    93  		return apitest.ProjectMeta{}, errors.New(msg.InternalServerError)
    94  	}
    95  
    96  	if resp.StatusCode != http.StatusOK {
    97  		body, _ := io.ReadAll(resp.Body)
    98  		return apitest.ProjectMeta{}, fmt.Errorf("request failed; unexpected response code:'%d', msg:'%v'", resp.StatusCode, string(body))
    99  	}
   100  
   101  	var project apitest.ProjectMeta
   102  	if err := json.NewDecoder(resp.Body).Decode(&project); err != nil {
   103  		return project, err
   104  	}
   105  	return project, nil
   106  }
   107  
   108  func (c *APITester) GetEventResult(ctx context.Context, hookID string, eventID string) (apitest.TestResult, error) {
   109  	if err := c.RequestRateLimiter.Wait(ctx); err != nil {
   110  		return apitest.TestResult{}, err
   111  	}
   112  
   113  	url := fmt.Sprintf("%s/api-testing/rest/v4/%s/insights/events/%s", c.URL, hookID, eventID)
   114  	req, err := NewRetryableRequestWithContext(ctx, http.MethodGet, url, nil)
   115  	if err != nil {
   116  		return apitest.TestResult{}, err
   117  	}
   118  	req.SetBasicAuth(c.Username, c.AccessKey)
   119  	resp, err := c.HTTPClient.Do(req)
   120  	if err != nil {
   121  		return apitest.TestResult{}, err
   122  	}
   123  	if resp.StatusCode >= http.StatusInternalServerError {
   124  		return apitest.TestResult{}, errors.New(msg.InternalServerError)
   125  	}
   126  	// 404 needs to be treated differently to ensure calling parent is aware of the specific error.
   127  	// API replies 404 until the event is fully processed.
   128  	if resp.StatusCode == http.StatusNotFound {
   129  		return apitest.TestResult{}, apitest.ErrEventNotFound
   130  	}
   131  	if resp.StatusCode != http.StatusOK {
   132  		body, _ := io.ReadAll(resp.Body)
   133  		return apitest.TestResult{}, fmt.Errorf("request failed; unexpected response code:'%d', msg:'%v'", resp.StatusCode, string(body))
   134  	}
   135  	var testResult apitest.TestResult
   136  	if err := json.NewDecoder(resp.Body).Decode(&testResult); err != nil {
   137  		return testResult, err
   138  	}
   139  	return testResult, nil
   140  }
   141  
   142  func (c *APITester) GetTest(ctx context.Context, hookID string, testID string) (apitest.Test, error) {
   143  	url := fmt.Sprintf("%s/api-testing/rest/v4/%s/tests/%s", c.URL, hookID, testID)
   144  	req, err := NewRetryableRequestWithContext(ctx, http.MethodGet, url, nil)
   145  	if err != nil {
   146  		return apitest.Test{}, err
   147  	}
   148  
   149  	req.SetBasicAuth(c.Username, c.AccessKey)
   150  	resp, err := c.HTTPClient.Do(req)
   151  	if err != nil {
   152  		return apitest.Test{}, err
   153  	}
   154  	defer resp.Body.Close()
   155  
   156  	if resp.StatusCode >= http.StatusInternalServerError {
   157  		return apitest.Test{}, errors.New(msg.InternalServerError)
   158  	}
   159  
   160  	if resp.StatusCode != http.StatusOK {
   161  		body, _ := io.ReadAll(resp.Body)
   162  		return apitest.Test{}, fmt.Errorf("request failed; unexpected response code:'%d', msg:'%v'", resp.StatusCode, string(body))
   163  	}
   164  
   165  	var test PublishedTest
   166  	if err := json.NewDecoder(resp.Body).Decode(&test); err != nil {
   167  		return test.Published, err
   168  	}
   169  	return test.Published, nil
   170  }
   171  
   172  func (c *APITester) composeURL(path string, buildID string, format string, tunnel config.Tunnel, taskID string) string {
   173  	// NOTE: API url is not user provided so skip error check
   174  	url, _ := url.Parse(c.URL)
   175  	url.Path = path
   176  
   177  	query := url.Query()
   178  	if buildID != "" {
   179  		query.Set("buildId", buildID)
   180  	}
   181  	if format != "" {
   182  		query.Set("format", format)
   183  	}
   184  
   185  	if tunnel.Name != "" {
   186  		var t string
   187  		if tunnel.Owner != "" {
   188  			t = fmt.Sprintf("%s:%s", tunnel.Owner, tunnel.Name)
   189  		} else {
   190  			t = fmt.Sprintf("%s:%s", c.Username, tunnel.Name)
   191  		}
   192  
   193  		query.Set("tunnelId", t)
   194  	}
   195  
   196  	if taskID != "" {
   197  		query.Set("taskId", taskID)
   198  	}
   199  
   200  	url.RawQuery = query.Encode()
   201  
   202  	return url.String()
   203  }
   204  
   205  // GetProjects returns the list of Project available.
   206  func (c *APITester) GetProjects(ctx context.Context) ([]apitest.ProjectMeta, error) {
   207  	url := fmt.Sprintf("%s/api-testing/api/project", c.URL)
   208  	req, err := NewRetryableRequestWithContext(ctx, http.MethodGet, url, nil)
   209  	if err != nil {
   210  		return []apitest.ProjectMeta{}, err
   211  	}
   212  
   213  	req.SetBasicAuth(c.Username, c.AccessKey)
   214  	resp, err := c.HTTPClient.Do(req)
   215  	if err != nil {
   216  		return []apitest.ProjectMeta{}, err
   217  	}
   218  	defer resp.Body.Close()
   219  
   220  	if resp.StatusCode >= http.StatusInternalServerError {
   221  		return []apitest.ProjectMeta{}, errors.New(msg.InternalServerError)
   222  	}
   223  
   224  	if resp.StatusCode != http.StatusOK {
   225  		body, _ := io.ReadAll(resp.Body)
   226  		return []apitest.ProjectMeta{}, fmt.Errorf("request failed; unexpected response code:'%d', msg:'%s'", resp.StatusCode, body)
   227  	}
   228  
   229  	var projects []apitest.ProjectMeta
   230  	if err := json.NewDecoder(resp.Body).Decode(&projects); err != nil {
   231  		return projects, err
   232  	}
   233  	return projects, nil
   234  }
   235  
   236  // GetHooks returns the list of hooks available.
   237  func (c *APITester) GetHooks(ctx context.Context, projectID string) ([]apitest.Hook, error) {
   238  	url := fmt.Sprintf("%s/api-testing/api/project/%s/hook", c.URL, projectID)
   239  	req, err := NewRetryableRequestWithContext(ctx, http.MethodGet, url, nil)
   240  	if err != nil {
   241  		return []apitest.Hook{}, err
   242  	}
   243  
   244  	req.SetBasicAuth(c.Username, c.AccessKey)
   245  	resp, err := c.HTTPClient.Do(req)
   246  	if err != nil {
   247  		return []apitest.Hook{}, err
   248  	}
   249  	defer resp.Body.Close()
   250  
   251  	if resp.StatusCode >= http.StatusInternalServerError {
   252  		return []apitest.Hook{}, errors.New(msg.InternalServerError)
   253  	}
   254  
   255  	if resp.StatusCode != http.StatusOK {
   256  		body, _ := io.ReadAll(resp.Body)
   257  		return []apitest.Hook{}, fmt.Errorf("request failed; unexpected response code:'%d', msg:'%s'", resp.StatusCode, body)
   258  	}
   259  
   260  	var hooks []apitest.Hook
   261  	if err := json.NewDecoder(resp.Body).Decode(&hooks); err != nil {
   262  		return hooks, err
   263  	}
   264  	return hooks, nil
   265  }
   266  
   267  // RunAllAsync runs all the tests for the project described by hookID and returns without waiting for their results.
   268  func (c *APITester) RunAllAsync(ctx context.Context, hookID string, buildID string, tunnel config.Tunnel, test apitest.TestRequest) (apitest.AsyncResponse, error) {
   269  	url := c.composeURL(fmt.Sprintf("/api-testing/rest/v4/%s/tests/_run-all", hookID), buildID, "", tunnel, "")
   270  
   271  	payload, err := json.Marshal(test)
   272  	if err != nil {
   273  		return apitest.AsyncResponse{}, err
   274  	}
   275  	payloadReader := bytes.NewReader(payload)
   276  
   277  	req, err := NewRetryableRequestWithContext(ctx, http.MethodPost, url, payloadReader)
   278  	if err != nil {
   279  		return apitest.AsyncResponse{}, err
   280  	}
   281  
   282  	req.SetBasicAuth(c.Username, c.AccessKey)
   283  
   284  	resp, err := c.doAsyncRun(c.HTTPClient, req)
   285  	if err != nil {
   286  		return apitest.AsyncResponse{}, err
   287  	}
   288  	return resp, nil
   289  }
   290  
   291  // RunEphemeralAsync runs the tests for the project described by hookID and returns without waiting for their results.
   292  func (c *APITester) RunEphemeralAsync(ctx context.Context, hookID string, buildID string, tunnel config.Tunnel, taskID string, test apitest.TestRequest) (apitest.AsyncResponse, error) {
   293  	url := c.composeURL(fmt.Sprintf("/api-testing/rest/v4/%s/tests/_exec", hookID), buildID, "", tunnel, "")
   294  
   295  	payload, err := json.Marshal(test)
   296  	if err != nil {
   297  		return apitest.AsyncResponse{}, err
   298  	}
   299  	payloadReader := bytes.NewReader(payload)
   300  
   301  	req, err := NewRetryableRequestWithContext(ctx, http.MethodPost, url, payloadReader)
   302  	if err != nil {
   303  		return apitest.AsyncResponse{}, err
   304  	}
   305  
   306  	req.SetBasicAuth(c.Username, c.AccessKey)
   307  
   308  	resp, err := c.doAsyncRun(c.HTTPClient, req)
   309  	if err != nil {
   310  		return apitest.AsyncResponse{}, err
   311  	}
   312  	return resp, nil
   313  }
   314  
   315  // RunTestAsync runs a single test described by testID for the project described by hookID and returns without waiting for results.
   316  func (c *APITester) RunTestAsync(ctx context.Context, hookID string, testID string, buildID string, tunnel config.Tunnel, test apitest.TestRequest) (apitest.AsyncResponse, error) {
   317  	url := c.composeURL(fmt.Sprintf("/api-testing/rest/v4/%s/tests/%s/_run", hookID, testID), buildID, "", tunnel, "")
   318  
   319  	payload, err := json.Marshal(test)
   320  	if err != nil {
   321  		return apitest.AsyncResponse{}, err
   322  	}
   323  	payloadReader := bytes.NewReader(payload)
   324  
   325  	req, err := NewRetryableRequestWithContext(ctx, http.MethodPost, url, payloadReader)
   326  	if err != nil {
   327  		return apitest.AsyncResponse{}, err
   328  	}
   329  
   330  	req.SetBasicAuth(c.Username, c.AccessKey)
   331  
   332  	resp, err := c.doAsyncRun(c.HTTPClient, req)
   333  	if err != nil {
   334  		return apitest.AsyncResponse{}, err
   335  	}
   336  
   337  	return resp, nil
   338  }
   339  
   340  // RunTagAsync runs all the tests for a testTag for a project described by hookID and returns without waiting for results.
   341  func (c *APITester) RunTagAsync(ctx context.Context, hookID string, testTag string, buildID string, tunnel config.Tunnel, test apitest.TestRequest) (apitest.AsyncResponse, error) {
   342  	url := c.composeURL(fmt.Sprintf("/api-testing/rest/v4/%s/tests/_tag/%s/_run", hookID, testTag), buildID, "", tunnel, "")
   343  
   344  	payload, err := json.Marshal(test)
   345  	if err != nil {
   346  		return apitest.AsyncResponse{}, err
   347  	}
   348  	payloadReader := bytes.NewReader(payload)
   349  
   350  	req, err := NewRetryableRequestWithContext(ctx, http.MethodPost, url, payloadReader)
   351  	if err != nil {
   352  		return apitest.AsyncResponse{}, err
   353  	}
   354  
   355  	req.SetBasicAuth(c.Username, c.AccessKey)
   356  
   357  	resp, err := c.doAsyncRun(c.HTTPClient, req)
   358  	if err != nil {
   359  		return apitest.AsyncResponse{}, err
   360  	}
   361  	return resp, nil
   362  }
   363  
   364  func (c *APITester) doAsyncRun(client *retryablehttp.Client, request *retryablehttp.Request) (apitest.AsyncResponse, error) {
   365  	request.Header.Set("Content-Type", "application/json")
   366  
   367  	resp, err := client.Do(request)
   368  	if err != nil {
   369  		return apitest.AsyncResponse{}, err
   370  	}
   371  	defer resp.Body.Close()
   372  
   373  	if resp.StatusCode >= http.StatusInternalServerError {
   374  		return apitest.AsyncResponse{}, errors.New(msg.InternalServerError)
   375  	}
   376  
   377  	if resp.StatusCode != http.StatusOK {
   378  		body, _ := io.ReadAll(resp.Body)
   379  		return apitest.AsyncResponse{}, fmt.Errorf("test execution failed; unexpected response code:'%d', msg:'%v'", resp.StatusCode, string(body))
   380  	}
   381  
   382  	var asyncResponse apitest.AsyncResponse
   383  	if err := json.NewDecoder(resp.Body).Decode(&asyncResponse); err != nil {
   384  		return apitest.AsyncResponse{}, err
   385  	}
   386  
   387  	return asyncResponse, nil
   388  }
   389  
   390  // GetVault returns the vault for the project identified by hookID
   391  func (c *APITester) GetVault(ctx context.Context, hookID string) (apitest.Vault, error) {
   392  	url := fmt.Sprintf("%s/api-testing/rest/v4/%s/vault", c.URL, hookID)
   393  	req, err := NewRetryableRequestWithContext(ctx, http.MethodGet, url, nil)
   394  	if err != nil {
   395  		return apitest.Vault{}, err
   396  	}
   397  
   398  	req.SetBasicAuth(c.Username, c.AccessKey)
   399  	resp, err := c.HTTPClient.Do(req)
   400  	if err != nil {
   401  		return apitest.Vault{}, err
   402  	}
   403  	defer resp.Body.Close()
   404  
   405  	if resp.StatusCode >= http.StatusInternalServerError {
   406  		return apitest.Vault{}, ErrServerError
   407  	}
   408  
   409  	if resp.StatusCode != http.StatusOK {
   410  		body, _ := io.ReadAll(resp.Body)
   411  		return apitest.Vault{}, fmt.Errorf("request failed; unexpected response code:'%d', msg:'%s'", resp.StatusCode, body)
   412  	}
   413  
   414  	var vaultResponse apitest.Vault
   415  	if err := json.NewDecoder(resp.Body).Decode(&vaultResponse); err != nil {
   416  		return apitest.Vault{}, err
   417  	}
   418  
   419  	return vaultResponse, nil
   420  }
   421  
   422  func (c *APITester) PutVault(ctx context.Context, hookID string, vault apitest.Vault) error {
   423  	url := fmt.Sprintf("%s/api-testing/rest/v4/%s/vault", c.URL, hookID)
   424  
   425  	var b bytes.Buffer
   426  	err := json.NewEncoder(&b).Encode(vault)
   427  	if err != nil {
   428  		return err
   429  	}
   430  
   431  	req, err := NewRetryableRequestWithContext(ctx, http.MethodPut, url, &b)
   432  	if err != nil {
   433  		return err
   434  	}
   435  
   436  	req.Header.Set("Content-Type", "application/json")
   437  	req.SetBasicAuth(c.Username, c.AccessKey)
   438  
   439  	resp, err := c.HTTPClient.Do(req)
   440  	if err != nil {
   441  		return err
   442  	}
   443  	defer resp.Body.Close()
   444  
   445  	if resp.StatusCode >= http.StatusInternalServerError {
   446  		return ErrServerError
   447  	}
   448  
   449  	if resp.StatusCode != http.StatusOK {
   450  		body, _ := io.ReadAll(resp.Body)
   451  		var errResp VaultErrResponse
   452  		if err = json.Unmarshal(body, &errResp); err != nil {
   453  			return fmt.Errorf("request failed; unexpected response code:'%d'; body: %q", resp.StatusCode, body)
   454  		}
   455  
   456  		return fmt.Errorf("request failed; unexpected response code: '%d'; err: '%v'", resp.StatusCode, errResp)
   457  	}
   458  
   459  	return nil
   460  }
   461  
   462  // ListVaultFiles returns the list of files in the vault for the project identified by projectID
   463  func (c *APITester) ListVaultFiles(ctx context.Context, projectID string) ([]apitest.VaultFile, error) {
   464  	filesURL := fmt.Sprintf("%s/api-testing/api/project/%s/drive/files", c.URL, projectID)
   465  	req, err := NewRetryableRequestWithContext(ctx, http.MethodGet, filesURL, nil)
   466  	if err != nil {
   467  		return []apitest.VaultFile{}, err
   468  	}
   469  
   470  	req.SetBasicAuth(c.Username, c.AccessKey)
   471  	resp, err := c.HTTPClient.Do(req)
   472  	if err != nil {
   473  		return []apitest.VaultFile{}, err
   474  	}
   475  	defer resp.Body.Close()
   476  
   477  	if resp.StatusCode >= http.StatusInternalServerError {
   478  		return []apitest.VaultFile{}, ErrServerError
   479  	}
   480  
   481  	if resp.StatusCode != http.StatusOK {
   482  		return []apitest.VaultFile{}, createError(resp.StatusCode, resp.Body)
   483  	}
   484  
   485  	var vaultResponse []apitest.VaultFile
   486  	if err := json.NewDecoder(resp.Body).Decode(&vaultResponse); err != nil {
   487  		return []apitest.VaultFile{}, err
   488  	}
   489  
   490  	return vaultResponse, nil
   491  }
   492  
   493  // GetVaultFileContent returns the content of a file in the vault for the project identified by projectID
   494  func (c *APITester) GetVaultFileContent(ctx context.Context, projectID string, fileID string) (io.ReadCloser, error) {
   495  	filesURL := fmt.Sprintf("%s/api-testing/api/project/%s/drive/files/%s", c.URL, projectID, fileID)
   496  	req, err := NewRetryableRequestWithContext(ctx, http.MethodGet, filesURL, nil)
   497  	if err != nil {
   498  		return nil, err
   499  	}
   500  
   501  	req.SetBasicAuth(c.Username, c.AccessKey)
   502  	resp, err := c.HTTPClient.Do(req)
   503  	if err != nil {
   504  		return nil, err
   505  	}
   506  
   507  	if resp.StatusCode >= http.StatusInternalServerError {
   508  		return nil, ErrServerError
   509  	}
   510  
   511  	if resp.StatusCode != http.StatusOK {
   512  		return nil, createError(resp.StatusCode, resp.Body)
   513  	}
   514  	return resp.Body, nil
   515  }
   516  
   517  // PutVaultFile stores the content of a file in the vault for the project identified by projectID
   518  func (c *APITester) PutVaultFile(ctx context.Context, projectID string, fileName string, fileBody io.ReadCloser) (apitest.VaultFile, error) {
   519  	multipartReader, contentType, err := multipartext.NewMultipartReader("file", fileName, "", fileBody)
   520  	if err != nil {
   521  		return apitest.VaultFile{}, nil
   522  	}
   523  
   524  	filesURL := fmt.Sprintf("%s/api-testing/api/project/%s/drive/files", c.URL, projectID)
   525  	req, err := NewRetryableRequestWithContext(ctx, http.MethodPost, filesURL, multipartReader)
   526  	if err != nil {
   527  		return apitest.VaultFile{}, err
   528  	}
   529  
   530  	req.Header.Set("Content-Type", contentType)
   531  	req.SetBasicAuth(c.Username, c.AccessKey)
   532  	resp, err := c.HTTPClient.Do(req)
   533  	if err != nil {
   534  		return apitest.VaultFile{}, err
   535  	}
   536  
   537  	if resp.StatusCode >= http.StatusInternalServerError {
   538  		return apitest.VaultFile{}, ErrServerError
   539  	}
   540  
   541  	if resp.StatusCode != http.StatusOK {
   542  		return apitest.VaultFile{}, createError(resp.StatusCode, resp.Body)
   543  	}
   544  
   545  	var vaultResponse apitest.VaultFile
   546  	if err := json.NewDecoder(resp.Body).Decode(&vaultResponse); err != nil {
   547  		return apitest.VaultFile{}, err
   548  	}
   549  
   550  	return vaultResponse, nil
   551  }
   552  
   553  // DeleteVaultFile delete the files in the vault for the project identified by projectID
   554  func (c *APITester) DeleteVaultFile(ctx context.Context, projectID string, fileNames []string) error {
   555  	filesURL := fmt.Sprintf("%s/api-testing/api/project/%s/drive/files/_delete", c.URL, projectID)
   556  
   557  	payload, err := json.Marshal(vaultFileDeletion{
   558  		FileNames: fileNames,
   559  	})
   560  	if err != nil {
   561  		return err
   562  	}
   563  
   564  	req, err := NewRetryableRequestWithContext(ctx, http.MethodPost, filesURL, bytes.NewReader(payload))
   565  	if err != nil {
   566  		return err
   567  	}
   568  	req.Header.Set("Content-Type", "application/json")
   569  	req.SetBasicAuth(c.Username, c.AccessKey)
   570  	resp, err := c.HTTPClient.Do(req)
   571  	if err != nil {
   572  		return err
   573  	}
   574  
   575  	if resp.StatusCode >= http.StatusInternalServerError {
   576  		return ErrServerError
   577  	}
   578  
   579  	if resp.StatusCode != http.StatusOK {
   580  		return createError(resp.StatusCode, resp.Body)
   581  	}
   582  	return nil
   583  }
   584  
   585  func createError(statusCode int, body io.Reader) error {
   586  	content, _ := io.ReadAll(body)
   587  
   588  	var errorDetails DriveErrResponse
   589  	if err := json.Unmarshal(content, &errorDetails); err != nil || errorDetails.Message == "" {
   590  		return fmt.Errorf("request failed; unexpected response code:'%d', body:'%s'", statusCode, content)
   591  	}
   592  	return fmt.Errorf("request failed; unexpected response code:'%d', msg:'%s'", statusCode, errorDetails.Message)
   593  }