github.com/swisscom/cloudfoundry-cli@v7.1.0+incompatible/cf/net/gateway_test.go (about)

     1  package net_test
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"log"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"net/url"
    12  	"os"
    13  	"reflect"
    14  	"runtime"
    15  	"strings"
    16  	"time"
    17  
    18  	"code.cloudfoundry.org/cli/cf/api/authentication"
    19  	"code.cloudfoundry.org/cli/cf/configuration/coreconfig"
    20  	"code.cloudfoundry.org/cli/cf/errors"
    21  	. "code.cloudfoundry.org/cli/cf/net"
    22  	"code.cloudfoundry.org/cli/cf/net/netfakes"
    23  	"code.cloudfoundry.org/cli/cf/terminal/terminalfakes"
    24  	"code.cloudfoundry.org/cli/cf/trace/tracefakes"
    25  	testconfig "code.cloudfoundry.org/cli/cf/util/testhelpers/configuration"
    26  	testnet "code.cloudfoundry.org/cli/cf/util/testhelpers/net"
    27  	"code.cloudfoundry.org/cli/version"
    28  	. "github.com/onsi/ginkgo"
    29  	. "github.com/onsi/gomega"
    30  	"github.com/onsi/gomega/ghttp"
    31  )
    32  
    33  var (
    34  	initialAccessToken   = testconfig.BuildTokenString(time.Now().AddDate(0, 0, 1))
    35  	refreshedAccessToken = testconfig.BuildTokenString(time.Now().AddDate(0, 1, 1))
    36  	refreshToken         = "refresh-token"
    37  )
    38  
    39  var _ = Describe("Gateway", func() {
    40  	var (
    41  		ccServer    *ghttp.Server
    42  		ccGateway   Gateway
    43  		uaaGateway  Gateway
    44  		config      coreconfig.ReadWriter
    45  		authRepo    authentication.Repository
    46  		currentTime time.Time
    47  		clock       func() time.Time
    48  
    49  		client *netfakes.FakeHTTPClientInterface
    50  	)
    51  
    52  	BeforeEach(func() {
    53  		currentTime = time.Unix(0, 0)
    54  		clock = func() time.Time { return currentTime }
    55  		config = testconfig.NewRepository()
    56  
    57  		ccGateway = NewCloudControllerGateway(config, clock, new(terminalfakes.FakeUI), new(tracefakes.FakePrinter), "")
    58  		ccGateway.PollingThrottle = 3 * time.Millisecond
    59  		uaaGateway = NewUAAGateway(config, new(terminalfakes.FakeUI), new(tracefakes.FakePrinter), "")
    60  	})
    61  
    62  	Describe("async timeout", func() {
    63  		Context("when the config has a positive async timeout", func() {
    64  			It("inherits the async timeout from the config", func() {
    65  				config.SetAsyncTimeout(9001)
    66  				ccGateway = NewCloudControllerGateway(config, time.Now, new(terminalfakes.FakeUI), new(tracefakes.FakePrinter), "")
    67  				Expect(ccGateway.AsyncTimeout()).To(Equal(9001 * time.Minute))
    68  			})
    69  		})
    70  	})
    71  
    72  	Describe("Connection errors", func() {
    73  		var oldNewHTTPClient func(tr *http.Transport, dumper RequestDumper) HTTPClientInterface
    74  
    75  		BeforeEach(func() {
    76  			client = new(netfakes.FakeHTTPClientInterface)
    77  
    78  			oldNewHTTPClient = NewHTTPClient
    79  			NewHTTPClient = func(tr *http.Transport, dumper RequestDumper) HTTPClientInterface {
    80  				return client
    81  			}
    82  		})
    83  
    84  		AfterEach(func() {
    85  			NewHTTPClient = oldNewHTTPClient
    86  		})
    87  
    88  		It("only retry when response body is nil and error occurred", func() {
    89  			client.DoReturns(&http.Response{Status: "internal error", StatusCode: 500}, errors.New("internal error"))
    90  			request, apiErr := ccGateway.NewRequest("GET", "https://example.com/v2/apps", "BEARER my-access-token", nil)
    91  			Expect(apiErr).ToNot(HaveOccurred())
    92  
    93  			_, apiErr = ccGateway.PerformRequest(request)
    94  			Expect(client.DoCallCount()).To(Equal(1))
    95  			Expect(apiErr).To(HaveOccurred())
    96  		})
    97  
    98  		It("Retries 3 times if we cannot contact the server", func() {
    99  			client.DoReturns(nil, errors.New("Connection refused"))
   100  			request, apiErr := ccGateway.NewRequest("GET", "https://example.com/v2/apps", "BEARER my-access-token", nil)
   101  			Expect(apiErr).ToNot(HaveOccurred())
   102  
   103  			_, apiErr = ccGateway.PerformRequest(request)
   104  			Expect(apiErr).To(HaveOccurred())
   105  			Expect(client.DoCallCount()).To(Equal(3))
   106  		})
   107  	})
   108  
   109  	Describe("NewRequest", func() {
   110  		var (
   111  			request *Request
   112  			apiErr  error
   113  		)
   114  
   115  		Context("when the body is nil", func() {
   116  			BeforeEach(func() {
   117  				request, apiErr = ccGateway.NewRequest("GET", "https://example.com/v2/apps", initialAccessToken, nil)
   118  				Expect(apiErr).NotTo(HaveOccurred())
   119  			})
   120  
   121  			It("does not use a ProgressReader as the SeekableBody", func() {
   122  				Expect(reflect.TypeOf(request.SeekableBody)).To(BeNil())
   123  			})
   124  
   125  			It("sets the Authorization header", func() {
   126  				Expect(request.HTTPReq.Header.Get("Authorization")).To(Equal(initialAccessToken))
   127  			})
   128  
   129  			It("sets the accept header to application/json", func() {
   130  				Expect(request.HTTPReq.Header.Get("accept")).To(Equal("application/json"))
   131  			})
   132  
   133  			It("sets the user agent header", func() {
   134  				Expect(request.HTTPReq.Header.Get("User-Agent")).To(Equal("go-cli " + version.VersionString() + " / " + runtime.GOOS))
   135  			})
   136  		})
   137  
   138  		Context("when the body is a file", func() {
   139  			BeforeEach(func() {
   140  				f, _ := os.Open("../../fixtures/test.file")
   141  				request, apiErr = ccGateway.NewRequestForFile("PUT", "https://example.com/v2/apps", initialAccessToken, f)
   142  				Expect(apiErr).NotTo(HaveOccurred())
   143  			})
   144  
   145  			It("Uses a ProgressReader as the SeekableBody", func() {
   146  				Expect(reflect.TypeOf(request.SeekableBody).String()).To(ContainSubstring("ProgressReader"))
   147  			})
   148  
   149  		})
   150  
   151  	})
   152  
   153  	Describe("PerformRequestForJSONResponse()", func() {
   154  		BeforeEach(func() {
   155  			ccServer = ghttp.NewServer()
   156  			ccServer.HTTPTestServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   157  			config.SetAPIEndpoint(ccServer.URL())
   158  		})
   159  
   160  		AfterEach(func() {
   161  			ccServer.Close()
   162  		})
   163  
   164  		Context("When CC response with an api error", func() {
   165  			BeforeEach(func() {
   166  				ccServer.AppendHandlers(
   167  					ghttp.CombineHandlers(
   168  						ghttp.VerifyRequest("GET", "/v2/some-endpoint"),
   169  						ghttp.VerifyHeaderKV("Connection", "close"),
   170  						ghttp.VerifyHeader(http.Header{
   171  							"accept": []string{"application/json"},
   172  						}),
   173  						ghttp.RespondWith(http.StatusUnauthorized, `{
   174    "code": 10003,
   175    "description": "You are not authorized to perform the requested action",
   176    "error_code": "CF-NotAuthorized"
   177  }`),
   178  					),
   179  				)
   180  			})
   181  
   182  			It("tries to unmarshal error response into provided resource", func() {
   183  				type apiErrResponse struct {
   184  					Code        int    `json:"code,omitempty"`
   185  					Description string `json:"description,omitempty"`
   186  				}
   187  
   188  				errResponse := new(apiErrResponse)
   189  				request, _ := ccGateway.NewRequest("GET", config.APIEndpoint()+"/v2/some-endpoint", config.AccessToken(), nil)
   190  				_, apiErr := ccGateway.PerformRequestForJSONResponse(request, errResponse)
   191  
   192  				Expect(apiErr).To(HaveOccurred())
   193  				Expect(errResponse.Code).To(Equal(10003))
   194  			})
   195  
   196  			It("ignores any unmarshal error and does not alter the api err response", func() {
   197  				request, _ := ccGateway.NewRequest("GET", config.APIEndpoint()+"/v2/some-endpoint", config.AccessToken(), nil)
   198  				_, apiErr := ccGateway.PerformRequestForJSONResponse(request, nil)
   199  
   200  				Expect(apiErr.Error()).To(Equal("Server error, status code: 401, error code: 10003, message: You are not authorized to perform the requested action"))
   201  			})
   202  
   203  		})
   204  
   205  	})
   206  
   207  	Describe("CRUD methods", func() {
   208  		Describe("Delete", func() {
   209  			var apiServer *httptest.Server
   210  
   211  			Describe("DeleteResourceSynchronously", func() {
   212  				var queryParams string
   213  				BeforeEach(func() {
   214  					apiServer = httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) {
   215  						queryParams = request.URL.RawQuery
   216  					}))
   217  					apiServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   218  					ccGateway.SetTrustedCerts(apiServer.TLS.Certificates)
   219  				})
   220  
   221  				It("does not send the async=true flag", func() {
   222  					err := ccGateway.DeleteResourceSynchronously(apiServer.URL, "/v2/foobars/SOME_GUID")
   223  					Expect(err).NotTo(HaveOccurred())
   224  					Expect(queryParams).ToNot(ContainSubstring("async=true"))
   225  				})
   226  
   227  				It("deletes a resource", func() {
   228  					err := ccGateway.DeleteResource(apiServer.URL, "/v2/foobars/SOME_GUID")
   229  					Expect(err).ToNot(HaveOccurred())
   230  				})
   231  			})
   232  
   233  			Context("when the config has an async timeout", func() {
   234  				BeforeEach(func() {
   235  					count := 0
   236  					apiServer = httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
   237  						switch request.URL.Path {
   238  						case "/v2/foobars/SOME_GUID":
   239  							writer.WriteHeader(http.StatusNoContent)
   240  						case "/v2/foobars/TIMEOUT":
   241  							currentTime = currentTime.Add(time.Minute * 31)
   242  							fmt.Fprintln(writer, `
   243  {
   244    "metadata": {
   245      "guid": "8438916f-5c00-4d44-a19b-1df65abe9d52",
   246      "created_at": "2014-05-15T19:15:01+00:00",
   247      "url": "/v2/jobs/8438916f-5c00-4d44-a19b-1df65abe9d52"
   248    },
   249    "entity": {
   250      "guid": "8438916f-5c00-4d44-a19b-1df65abe9d52",
   251      "status": "queued"
   252    }
   253  }`)
   254  							writer.WriteHeader(http.StatusAccepted)
   255  						case "/v2/jobs/8438916f-5c00-4d44-a19b-1df65abe9d52":
   256  							if count == 0 {
   257  								count++
   258  								currentTime = currentTime.Add(time.Minute * 31)
   259  
   260  								writer.WriteHeader(http.StatusOK)
   261  								fmt.Fprintln(writer, `
   262  {
   263    "entity": {
   264      "guid": "8438916f-5c00-4d44-a19b-1df65abe9d52",
   265      "status": "queued"
   266    }
   267  }`)
   268  							} else {
   269  								panic("FAIL")
   270  							}
   271  						default:
   272  							panic("shouldn't have made call to this URL: " + request.URL.Path)
   273  						}
   274  					}))
   275  
   276  					config.SetAsyncTimeout(30)
   277  					ccGateway.SetTrustedCerts(apiServer.TLS.Certificates)
   278  					apiServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   279  				})
   280  
   281  				AfterEach(func() {
   282  					apiServer.Close()
   283  				})
   284  
   285  				It("deletes a resource", func() {
   286  					err := ccGateway.DeleteResource(apiServer.URL, "/v2/foobars/SOME_GUID")
   287  					Expect(err).ToNot(HaveOccurred())
   288  				})
   289  
   290  				Context("when the request would take longer than the async timeout", func() {
   291  					It("returns an error", func() {
   292  						apiErr := ccGateway.DeleteResource(apiServer.URL, "/v2/foobars/TIMEOUT")
   293  						Expect(apiErr).To(HaveOccurred())
   294  						Expect(apiErr).To(BeAssignableToTypeOf(errors.NewAsyncTimeoutError("http://some.url")))
   295  					})
   296  				})
   297  			})
   298  		})
   299  	})
   300  
   301  	Describe("making an async request", func() {
   302  		var (
   303  			jobStatus     string
   304  			apiServer     *httptest.Server
   305  			authServer    *httptest.Server
   306  			statusChannel chan string
   307  		)
   308  
   309  		BeforeEach(func() {
   310  			jobStatus = "queued"
   311  			statusChannel = make(chan string, 10)
   312  
   313  			apiServer = httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
   314  				currentTime = currentTime.Add(time.Millisecond * 11)
   315  
   316  				updateStatus, ok := <-statusChannel
   317  				if ok {
   318  					jobStatus = updateStatus
   319  				}
   320  
   321  				switch request.URL.Path {
   322  				case "/v2/foo":
   323  					fmt.Fprintln(writer, `{ "metadata": { "url": "/v2/jobs/the-job-guid" } }`)
   324  				case "/v2/jobs/the-job-guid":
   325  					fmt.Fprintf(writer, `
   326  					{
   327  						"entity": {
   328  							"status": "%s",
   329  							"error_details": {
   330  								"description": "he's dead, Jim"
   331  							}
   332  						}
   333  					}`, jobStatus)
   334  				default:
   335  					writer.WriteHeader(http.StatusInternalServerError)
   336  					fmt.Fprintf(writer, `"Unexpected request path '%s'"`, request.URL.Path)
   337  				}
   338  			}))
   339  
   340  			authServer, _ = testnet.NewTLSServer([]testnet.TestRequest{})
   341  
   342  			config, authRepo = createAuthenticationRepository(apiServer, authServer)
   343  			ccGateway.SetTokenRefresher(authRepo)
   344  
   345  			ccGateway.SetTrustedCerts(apiServer.TLS.Certificates)
   346  			apiServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   347  			authServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   348  		})
   349  
   350  		AfterEach(func() {
   351  			apiServer.Close()
   352  			authServer.Close()
   353  		})
   354  
   355  		It("returns the last response if the job completes before the timeout", func() {
   356  			go func() {
   357  				statusChannel <- "queued"
   358  				statusChannel <- "finished"
   359  			}()
   360  
   361  			request, _ := ccGateway.NewRequest("GET", config.APIEndpoint()+"/v2/foo", config.AccessToken(), nil)
   362  			_, apiErr := ccGateway.PerformPollingRequestForJSONResponse(config.APIEndpoint(), request, new(struct{}), 500*time.Millisecond)
   363  			Expect(apiErr).NotTo(HaveOccurred())
   364  		})
   365  
   366  		It("returns an error with the right message when the job fails", func() {
   367  			go func() {
   368  				statusChannel <- "queued"
   369  				statusChannel <- "failed"
   370  			}()
   371  
   372  			request, _ := ccGateway.NewRequest("GET", config.APIEndpoint()+"/v2/foo", config.AccessToken(), nil)
   373  			_, apiErr := ccGateway.PerformPollingRequestForJSONResponse(config.APIEndpoint(), request, new(struct{}), 500*time.Millisecond)
   374  			Expect(apiErr.Error()).To(ContainSubstring("he's dead, Jim"))
   375  		})
   376  
   377  		It("returns an error if jobs takes longer than the timeout", func() {
   378  			go func() {
   379  				statusChannel <- "queued"
   380  				statusChannel <- "OHNOES"
   381  			}()
   382  			request, _ := ccGateway.NewRequest("GET", config.APIEndpoint()+"/v2/foo", config.AccessToken(), nil)
   383  			_, apiErr := ccGateway.PerformPollingRequestForJSONResponse(config.APIEndpoint(), request, new(struct{}), 10*time.Millisecond)
   384  			Expect(apiErr).To(HaveOccurred())
   385  			Expect(apiErr).To(BeAssignableToTypeOf(errors.NewAsyncTimeoutError("http://some.url")))
   386  		})
   387  	})
   388  
   389  	Describe("when uploading a file", func() {
   390  		var (
   391  			err          error
   392  			request      *Request
   393  			apiErr       error
   394  			apiServer    *httptest.Server
   395  			authServer   *httptest.Server
   396  			fileToUpload *os.File
   397  		)
   398  
   399  		BeforeEach(func() {
   400  			apiServer = httptest.NewTLSServer(refreshTokenAPIEndPoint(
   401  				`{ "code": 1000, "description": "Auth token is invalid" }`,
   402  				testnet.TestResponse{Status: http.StatusOK},
   403  			))
   404  
   405  			authServer = httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
   406  				fmt.Fprintln(
   407  					writer,
   408  					fmt.Sprintf(`{ "access_token": "%s", "token_type": "bearer", "refresh_token": "new-refresh-token"}`, refreshedAccessToken))
   409  			}))
   410  			apiServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   411  			authServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   412  
   413  			fileToUpload, err = ioutil.TempFile("", "test-gateway")
   414  			strings.NewReader("expected body").WriteTo(fileToUpload)
   415  
   416  			config, auth := createAuthenticationRepository(apiServer, authServer)
   417  			ccGateway.SetTokenRefresher(auth)
   418  			ccGateway.SetTrustedCerts(apiServer.TLS.Certificates)
   419  
   420  			request, apiErr = ccGateway.NewRequestForFile("POST", config.APIEndpoint()+"/v2/foo", config.AccessToken(), fileToUpload)
   421  		})
   422  
   423  		AfterEach(func() {
   424  			apiServer.Close()
   425  			authServer.Close()
   426  			fileToUpload.Close()
   427  			os.Remove(fileToUpload.Name())
   428  		})
   429  
   430  		It("sets the content length to the size of the file", func() {
   431  			Expect(err).NotTo(HaveOccurred())
   432  			Expect(apiErr).NotTo(HaveOccurred())
   433  			Expect(request.HTTPReq.ContentLength).To(Equal(int64(13)))
   434  		})
   435  	})
   436  
   437  	Describe("refreshing the auth token", func() {
   438  		var (
   439  			authServer  *httptest.Server
   440  			requestPath = "/v2/foo"
   441  		)
   442  
   443  		AfterEach(func() {
   444  			authServer.Close()
   445  		})
   446  
   447  		It("is done when the token is expired", func() {
   448  			authServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   449  				fmt.Fprintln(w, fmt.Sprintf(`{
   450  					"access_token": "%s",
   451  					"token_type": "bearer",
   452  					"refresh_token": "%s"
   453  				}`, refreshedAccessToken, refreshToken))
   454  			}))
   455  			uaaGateway.SetTrustedCerts(authServer.TLS.Certificates)
   456  			authServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   457  
   458  			requestPath := "/v2/foo"
   459  			apiServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   460  				// refresh call
   461  				if r.Header.Get("Authorization") == "bearer "+refreshedAccessToken && r.URL.Path == requestPath {
   462  					return // 200 with no body
   463  				} else {
   464  					http.Error(w, "unexpected request", http.StatusTeapot)
   465  				}
   466  			}))
   467  
   468  			defer apiServer.Close()
   469  			ccGateway.SetTrustedCerts(apiServer.TLS.Certificates)
   470  
   471  			config, auth := createAuthenticationRepository(apiServer, authServer)
   472  			expiredToken := testconfig.BuildTokenString(time.Time{})
   473  			config.SetAccessToken("bearer " + expiredToken)
   474  			uaaGateway.SetTokenRefresher(auth)
   475  			request, apiErr := uaaGateway.NewRequest("POST", config.APIEndpoint()+requestPath, "bearer "+expiredToken, strings.NewReader("expected body"))
   476  			_, apiErr = uaaGateway.PerformRequest(request)
   477  
   478  			Expect(apiErr).NotTo(HaveOccurred())
   479  			Expect(config.AccessToken()).To(Equal("bearer " + refreshedAccessToken))
   480  			Expect(config.RefreshToken()).To(Equal(refreshToken))
   481  		})
   482  
   483  		It("is not done when the token is still valid", func() {
   484  			authServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   485  				http.Error(w, "dont call me", http.StatusTeapot)
   486  			}))
   487  			uaaGateway.SetTrustedCerts(authServer.TLS.Certificates)
   488  			authServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   489  
   490  			apiServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   491  				if r.Header.Get("Authorization") == "bearer "+initialAccessToken && r.URL.Path == requestPath {
   492  					return // 200 with no body
   493  				} else {
   494  					http.Error(w, "unexpected request", http.StatusTeapot)
   495  				}
   496  			}))
   497  			defer apiServer.Close()
   498  			ccGateway.SetTrustedCerts(apiServer.TLS.Certificates)
   499  
   500  			config, auth := createAuthenticationRepository(apiServer, authServer)
   501  			config.SetAccessToken("bearer " + initialAccessToken)
   502  			uaaGateway.SetTokenRefresher(auth)
   503  			request, apiErr := uaaGateway.NewRequest("POST", config.APIEndpoint()+requestPath, "bearer "+initialAccessToken, strings.NewReader("some body"))
   504  			_, apiErr = uaaGateway.PerformRequest(request)
   505  
   506  			Expect(apiErr).NotTo(HaveOccurred())
   507  			Expect(config.AccessToken()).To(Equal("bearer " + initialAccessToken))
   508  		})
   509  
   510  	})
   511  
   512  	Describe("SSL certificate validation errors", func() {
   513  		var (
   514  			request   *Request
   515  			apiServer *httptest.Server
   516  		)
   517  
   518  		BeforeEach(func() {
   519  			apiServer = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
   520  				fmt.Fprintln(w, `{}`)
   521  			}))
   522  			apiServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   523  			request, _ = ccGateway.NewRequest("POST", apiServer.URL+"/v2/foo", "the-access-token", nil)
   524  		})
   525  
   526  		AfterEach(func() {
   527  			apiServer.Close()
   528  		})
   529  
   530  		Context("when SSL validation is enabled", func() {
   531  			It("returns an invalid cert error if the server's CA is unknown (e.g. cert is self-signed)", func() {
   532  				apiServer.TLS.Certificates = []tls.Certificate{testnet.MakeSelfSignedTLSCert()}
   533  
   534  				_, apiErr := ccGateway.PerformRequest(request)
   535  				certErr, ok := apiErr.(*errors.InvalidSSLCert)
   536  				Expect(ok).To(BeTrue())
   537  				Expect(certErr.URL).To(Equal(getHost(apiServer.URL)))
   538  				Expect(certErr.Reason).To(Equal("unknown authority"))
   539  			})
   540  
   541  			It("returns an invalid cert error if the server's cert doesn't match its host", func() {
   542  				apiServer.TLS.Certificates = []tls.Certificate{testnet.MakeTLSCertWithInvalidHost()}
   543  
   544  				_, apiErr := ccGateway.PerformRequest(request)
   545  				certErr, ok := apiErr.(*errors.InvalidSSLCert)
   546  				Expect(ok).To(BeTrue())
   547  				Expect(certErr.URL).To(Equal(getHost(apiServer.URL)))
   548  				if runtime.GOOS != "windows" {
   549  					Expect(certErr.Reason).To(Equal("not valid for the requested host"))
   550  				}
   551  			})
   552  
   553  			It("returns an invalid cert error if the server's cert has expired", func() {
   554  				apiServer.TLS.Certificates = []tls.Certificate{testnet.MakeExpiredTLSCert()}
   555  
   556  				_, apiErr := ccGateway.PerformRequest(request)
   557  				certErr, ok := apiErr.(*errors.InvalidSSLCert)
   558  				Expect(ok).To(BeTrue())
   559  				Expect(certErr.URL).To(Equal(getHost(apiServer.URL)))
   560  				if runtime.GOOS != "windows" {
   561  					Expect(certErr.Reason).To(Equal(""))
   562  				}
   563  			})
   564  		})
   565  
   566  		Context("when SSL validation is disabled", func() {
   567  			BeforeEach(func() {
   568  				apiServer.TLS.Certificates = []tls.Certificate{testnet.MakeExpiredTLSCert()}
   569  				config.SetSSLDisabled(true)
   570  			})
   571  
   572  			It("succeeds", func() {
   573  				_, apiErr := ccGateway.PerformRequest(request)
   574  				Expect(apiErr).NotTo(HaveOccurred())
   575  			})
   576  		})
   577  
   578  	})
   579  
   580  	Describe("collecting warnings", func() {
   581  		var (
   582  			apiServer  *httptest.Server
   583  			authServer *httptest.Server
   584  		)
   585  
   586  		BeforeEach(func() {
   587  			apiServer = httptest.NewTLSServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
   588  				switch request.URL.Path {
   589  				case "/v2/happy":
   590  					fmt.Fprintln(writer, `{ "metadata": { "url": "/v2/jobs/the-job-guid" } }`)
   591  				case "/v2/warning1":
   592  					writer.Header().Add("X-Cf-Warnings", url.QueryEscape("Something not too awful has happened"))
   593  					fmt.Fprintln(writer, `{ "metadata": { "url": "/v2/jobs/the-job-guid" } }`)
   594  				case "/v2/warning2":
   595  					writer.Header().Add("X-Cf-Warnings", url.QueryEscape("Something a little awful"))
   596  					writer.WriteHeader(http.StatusInternalServerError)
   597  					fmt.Fprintf(writer, `{ "key": "value" }`)
   598  				}
   599  			}))
   600  
   601  			authServer, _ = testnet.NewTLSServer([]testnet.TestRequest{})
   602  
   603  			config, authRepo = createAuthenticationRepository(apiServer, authServer)
   604  			ccGateway.SetTokenRefresher(authRepo)
   605  
   606  			ccGateway.SetTrustedCerts(apiServer.TLS.Certificates)
   607  			apiServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   608  			authServer.Config.ErrorLog = log.New(&bytes.Buffer{}, "", 0)
   609  
   610  			config, authRepo = createAuthenticationRepository(apiServer, authServer)
   611  		})
   612  
   613  		AfterEach(func() {
   614  			apiServer.Close()
   615  			authServer.Close()
   616  		})
   617  
   618  		It("saves all X-Cf-Warnings headers and exposes them", func() {
   619  			request, _ := ccGateway.NewRequest("GET", config.APIEndpoint()+"/v2/happy", config.AccessToken(), nil)
   620  			ccGateway.PerformRequest(request)
   621  			request, _ = ccGateway.NewRequest("GET", config.APIEndpoint()+"/v2/warning1", config.AccessToken(), nil)
   622  			ccGateway.PerformRequest(request)
   623  			request, _ = ccGateway.NewRequest("GET", config.APIEndpoint()+"/v2/warning2", config.AccessToken(), nil)
   624  			ccGateway.PerformRequest(request)
   625  
   626  			Expect(ccGateway.Warnings()).To(Equal(
   627  				[]string{"Something not too awful has happened", "Something a little awful"},
   628  			))
   629  		})
   630  
   631  		It("defaults warnings to an empty slice", func() {
   632  			Expect(ccGateway.Warnings()).ToNot(BeNil())
   633  		})
   634  	})
   635  })
   636  
   637  func getHost(urlString string) string {
   638  	url, err := url.Parse(urlString)
   639  	Expect(err).NotTo(HaveOccurred())
   640  	return url.Host
   641  }
   642  
   643  func refreshTokenAPIEndPoint(unauthorizedBody string, secondReqResp testnet.TestResponse) http.HandlerFunc {
   644  	return func(writer http.ResponseWriter, request *http.Request) {
   645  		var jsonResponse string
   646  
   647  		bodyBytes, err := ioutil.ReadAll(request.Body)
   648  		if err != nil || string(bodyBytes) != "expected body" {
   649  			writer.WriteHeader(http.StatusInternalServerError)
   650  			return
   651  		}
   652  
   653  		switch request.Header.Get("Authorization") {
   654  		case initialAccessToken:
   655  			writer.WriteHeader(http.StatusUnauthorized)
   656  			jsonResponse = unauthorizedBody
   657  		case refreshedAccessToken:
   658  			writer.WriteHeader(secondReqResp.Status)
   659  			jsonResponse = secondReqResp.Body
   660  		default:
   661  			writer.WriteHeader(http.StatusInternalServerError)
   662  		}
   663  
   664  		fmt.Fprintln(writer, jsonResponse)
   665  	}
   666  }
   667  
   668  func createAuthenticationRepository(apiServer *httptest.Server, authServer *httptest.Server) (coreconfig.ReadWriter, authentication.Repository) {
   669  	config := testconfig.NewRepository()
   670  	config.SetAuthenticationEndpoint(authServer.URL)
   671  	config.SetAPIEndpoint(apiServer.URL)
   672  
   673  	authGateway := NewUAAGateway(config, new(terminalfakes.FakeUI), new(tracefakes.FakePrinter), "")
   674  	authGateway.SetTrustedCerts(authServer.TLS.Certificates)
   675  
   676  	fakePrinter := new(tracefakes.FakePrinter)
   677  	dumper := NewRequestDumper(fakePrinter)
   678  	authenticator := authentication.NewUAARepository(authGateway, config, dumper)
   679  
   680  	return config, authenticator
   681  }