github.com/openshift-online/ocm-sdk-go@v0.1.473/methods_test.go (about)

     1  /*
     2  Copyright (c) 2019 Red Hat, Inc.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8    http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // This file contains tests for the methods that request tokens.
    18  
    19  package sdk
    20  
    21  import (
    22  	"context"
    23  	"errors"
    24  	"net/http"
    25  	"os"
    26  	"time"
    27  
    28  	"github.com/onsi/gomega/ghttp"
    29  
    30  	. "github.com/onsi/ginkgo/v2/dsl/core"             // nolint
    31  	. "github.com/onsi/gomega"                         // nolint
    32  	. "github.com/openshift-online/ocm-sdk-go/testing" // nolint
    33  
    34  	cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
    35  )
    36  
    37  var _ = Describe("Methods", func() {
    38  	// Servers used during the tests:
    39  	var oidServer *ghttp.Server
    40  	var apiServer *ghttp.Server
    41  
    42  	// Names of the temporary files containing the CAs for the servers:
    43  	var oidCA string
    44  	var apiCA string
    45  
    46  	// Connection used during the tests:
    47  	var connection *Connection
    48  
    49  	BeforeEach(func() {
    50  		var err error
    51  
    52  		// Create the tokens:
    53  		accessToken := MakeTokenString("Bearer", 5*time.Minute)
    54  		refreshToken := MakeTokenString("Refresh", 10*time.Hour)
    55  
    56  		// Create the OpenID server:
    57  		oidServer, oidCA = MakeTCPTLSServer()
    58  		oidServer.AppendHandlers(
    59  			ghttp.CombineHandlers(
    60  				RespondWithAccessAndRefreshTokens(accessToken, refreshToken),
    61  			),
    62  		)
    63  
    64  		// Create the API server:
    65  		apiServer, apiCA = MakeTCPTLSServer()
    66  
    67  		// Create the connection:
    68  		connection, err = NewConnectionBuilder().
    69  			Logger(logger).
    70  			TokenURL(oidServer.URL()).
    71  			URL(apiServer.URL()).
    72  			Tokens(refreshToken).
    73  			TrustedCAFile(oidCA).
    74  			TrustedCAFile(apiCA).
    75  			RetryLimit(0).
    76  			Build()
    77  		Expect(err).ToNot(HaveOccurred())
    78  	})
    79  
    80  	AfterEach(func() {
    81  		// Stop the servers:
    82  		oidServer.Close()
    83  		apiServer.Close()
    84  
    85  		// Close the connection:
    86  		err := connection.Close()
    87  		Expect(err).ToNot(HaveOccurred())
    88  
    89  		// Remove the temporary CA files:
    90  		err = os.Remove(oidCA)
    91  		Expect(err).ToNot(HaveOccurred())
    92  		err = os.Remove(apiCA)
    93  		Expect(err).ToNot(HaveOccurred())
    94  	})
    95  
    96  	Describe("Get", func() {
    97  		It("Sends path", func() {
    98  			// Configure the server:
    99  			apiServer.AppendHandlers(
   100  				ghttp.CombineHandlers(
   101  					ghttp.VerifyRequest(http.MethodGet, "/mypath"),
   102  					RespondWithJSON(http.StatusOK, ""),
   103  				),
   104  			)
   105  
   106  			// Send the request:
   107  			_, err := connection.Get().
   108  				Path("/mypath").
   109  				Send()
   110  			Expect(err).ToNot(HaveOccurred())
   111  		})
   112  
   113  		It("Sends accept header", func() {
   114  			// Configure the server:
   115  			apiServer.AppendHandlers(
   116  				ghttp.CombineHandlers(
   117  					ghttp.VerifyHeaderKV("Accept", "application/json"),
   118  					RespondWithJSON(http.StatusOK, ""),
   119  				),
   120  			)
   121  
   122  			// Send the request:
   123  			_, err := connection.Get().
   124  				Path("/mypath").
   125  				Send()
   126  			Expect(err).ToNot(HaveOccurred())
   127  		})
   128  
   129  		It("Sends one query parameter", func() {
   130  			// Configure the server:
   131  			apiServer.AppendHandlers(
   132  				ghttp.CombineHandlers(
   133  					ghttp.VerifyFormKV("myparameter", "myvalue"),
   134  					RespondWithJSON(http.StatusOK, ""),
   135  				),
   136  			)
   137  
   138  			// Send the request:
   139  			_, err := connection.Get().
   140  				Path("/mypath").
   141  				Parameter("myparameter", "myvalue").
   142  				Send()
   143  			Expect(err).ToNot(HaveOccurred())
   144  		})
   145  
   146  		It("Sends two query parameters", func() {
   147  			// Configure the server:
   148  			apiServer.AppendHandlers(
   149  				ghttp.CombineHandlers(
   150  					ghttp.VerifyFormKV("myparameter", "myvalue"),
   151  					ghttp.VerifyFormKV("yourparameter", "yourvalue"),
   152  					RespondWithJSON(http.StatusOK, ""),
   153  				),
   154  			)
   155  
   156  			// Send the request:
   157  			_, err := connection.Get().
   158  				Path("/mypath").
   159  				Parameter("myparameter", "myvalue").
   160  				Parameter("yourparameter", "yourvalue").
   161  				Send()
   162  			Expect(err).ToNot(HaveOccurred())
   163  		})
   164  
   165  		It("Sends one header", func() {
   166  			// Configure the server:
   167  			apiServer.AppendHandlers(
   168  				ghttp.CombineHandlers(
   169  					ghttp.VerifyHeaderKV("myheader", "myvalue"),
   170  					RespondWithJSON(http.StatusOK, ""),
   171  				),
   172  			)
   173  
   174  			// Send the request:
   175  			_, err := connection.Get().
   176  				Path("/mypath").
   177  				Header("myheader", "myvalue").
   178  				Send()
   179  			Expect(err).ToNot(HaveOccurred())
   180  		})
   181  
   182  		It("Sends two headers", func() {
   183  			// Configure the server:
   184  			apiServer.AppendHandlers(
   185  				ghttp.CombineHandlers(
   186  					ghttp.VerifyHeaderKV("myheader", "myvalue"),
   187  					ghttp.VerifyHeaderKV("yourheader", "yourvalue"),
   188  					RespondWithJSON(http.StatusOK, ""),
   189  				),
   190  			)
   191  
   192  			// Send the request:
   193  			_, err := connection.Get().
   194  				Path("/mypath").
   195  				Header("myheader", "myvalue").
   196  				Header("yourheader", "yourvalue").
   197  				Send()
   198  			Expect(err).ToNot(HaveOccurred())
   199  		})
   200  
   201  		It("Receives body", func() {
   202  			// Configure the server:
   203  			apiServer.AppendHandlers(
   204  				RespondWithJSON(http.StatusOK, `{"test":"mybody"}`),
   205  			)
   206  
   207  			// Send the request:
   208  			response, err := connection.Get().
   209  				Path("/mypath").
   210  				Send()
   211  			Expect(err).ToNot(HaveOccurred())
   212  			Expect(response).ToNot(BeNil())
   213  			Expect(response.Status()).To(Equal(http.StatusOK))
   214  			Expect(response.String()).To(Equal(`{"test":"mybody"}`))
   215  			Expect(response.Bytes()).To(Equal([]byte(`{"test":"mybody"}`)))
   216  		})
   217  
   218  		It("Receives status code 200", func() {
   219  			// Configure the server:
   220  			apiServer.AppendHandlers(
   221  				RespondWithJSON(http.StatusOK, ""),
   222  			)
   223  
   224  			// Send the request:
   225  			response, err := connection.Get().
   226  				Path("/mypath").
   227  				Send()
   228  			Expect(err).ToNot(HaveOccurred())
   229  			Expect(response).ToNot(BeNil())
   230  			Expect(response.Status()).To(Equal(http.StatusOK))
   231  		})
   232  
   233  		It("Receives status code 400", func() {
   234  			// Configure the server:
   235  			apiServer.AppendHandlers(
   236  				RespondWithJSON(http.StatusBadRequest, ""),
   237  			)
   238  
   239  			// Send the request:
   240  			response, err := connection.Get().
   241  				Path("/mypath").
   242  				Send()
   243  			Expect(err).ToNot(HaveOccurred())
   244  			Expect(response).ToNot(BeNil())
   245  			Expect(response.Status()).To(Equal(http.StatusBadRequest))
   246  		})
   247  
   248  		It("Receives status code 500", func() {
   249  			// Configure the server:
   250  			apiServer.AppendHandlers(
   251  				RespondWithJSON(http.StatusInternalServerError, ""),
   252  			)
   253  
   254  			// Send the request:
   255  			response, err := connection.Get().
   256  				Path("/mypath").
   257  				Send()
   258  			Expect(err).ToNot(HaveOccurred())
   259  			Expect(response).ToNot(BeNil())
   260  			Expect(response.Status()).To(Equal(http.StatusInternalServerError))
   261  		})
   262  
   263  		It("Fails if no path is given", func() {
   264  			response, err := connection.Get().
   265  				Send()
   266  			Expect(err).To(HaveOccurred())
   267  			Expect(err.Error()).To(ContainSubstring("path"))
   268  			Expect(err.Error()).To(ContainSubstring("mandatory"))
   269  			Expect(response).To(BeNil())
   270  		})
   271  
   272  		It("Honors cookies", func() {
   273  			// Configure the server:
   274  			apiServer.AppendHandlers(
   275  				ghttp.CombineHandlers(
   276  					RespondWithCookie("mycookie", "myvalue"),
   277  					RespondWithJSONTemplate(http.StatusOK, "{}"),
   278  				),
   279  				ghttp.CombineHandlers(
   280  					VerifyCookie("mycookie", "myvalue"),
   281  					RespondWithJSONTemplate(http.StatusOK, "{}"),
   282  				),
   283  			)
   284  
   285  			// Send first request. The server will respond setting a cookie.
   286  			_, err := connection.Get().
   287  				Path("/mypath").
   288  				Send()
   289  			Expect(err).ToNot(HaveOccurred())
   290  
   291  			// Send second request, which should include the cookie returned by the
   292  			// server in the first response.
   293  			_, err = connection.Get().
   294  				Path("/mypath").
   295  				Send()
   296  			Expect(err).ToNot(HaveOccurred())
   297  		})
   298  
   299  		It("Wraps deadline exceeded error", func() {
   300  			// Configure the server so that it introduces an artificial delay:
   301  			apiServer.AppendHandlers(
   302  				ghttp.CombineHandlers(
   303  					http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   304  						time.Sleep(100 * time.Millisecond)
   305  					}),
   306  					RespondWithJSON(http.StatusOK, ""),
   307  				),
   308  			)
   309  
   310  			// Send the request with a timeout smaller than the artificial delay
   311  			// introduced by the server so that a deadline exceeded error will be
   312  			// created and returned:
   313  			ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
   314  			defer cancel()
   315  			_, err := connection.Get().
   316  				Path("/mypath").
   317  				SendContext(ctx)
   318  			Expect(err).To(HaveOccurred())
   319  			Expect(errors.Is(err, context.DeadlineExceeded)).To(BeTrue())
   320  		})
   321  
   322  		It("Uses HTTP/2", func() {
   323  			// Configure the server:
   324  			apiServer.AppendHandlers(
   325  				ghttp.CombineHandlers(
   326  					http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   327  						Expect(r.Proto).To(Equal("HTTP/2.0"))
   328  					}),
   329  					RespondWithJSON(http.StatusOK, ""),
   330  				),
   331  			)
   332  
   333  			// Send the request:
   334  			_, err := connection.Get().
   335  				Path("/mypath").
   336  				Send()
   337  			Expect(err).ToNot(HaveOccurred())
   338  		})
   339  	})
   340  
   341  	Describe("Post", func() {
   342  		It("Accepts empty body", func() {
   343  			// Configure the server:
   344  			apiServer.AppendHandlers(
   345  				RespondWithJSON(http.StatusOK, ""),
   346  			)
   347  
   348  			// Send the request:
   349  			response, err := connection.Post().
   350  				Path("/mypath").
   351  				Send()
   352  			Expect(err).ToNot(HaveOccurred())
   353  			Expect(response).ToNot(BeNil())
   354  			Expect(response.Status()).To(Equal(http.StatusOK))
   355  		})
   356  
   357  		It("Accepts 204 with empty body", func() {
   358  			// Configure the server:
   359  			apiServer.AppendHandlers(
   360  				RespondWithJSON(http.StatusNoContent, ""),
   361  			)
   362  
   363  			// Prepare the body:
   364  			body, err := cmv1.NewUpgradePolicy().
   365  				ScheduleType("my-type").
   366  				Version("my-version").
   367  				Build()
   368  			Expect(err).ToNot(HaveOccurred())
   369  
   370  			// Send the request:
   371  			collection := connection.ClustersMgmt().V1().
   372  				Clusters().
   373  				Cluster("123").
   374  				UpgradePolicies()
   375  			response, err := collection.Add().
   376  				Body(body).
   377  				Parameter("dryRun", true).
   378  				Send()
   379  			Expect(err).ToNot(HaveOccurred())
   380  			Expect(response).ToNot(BeNil())
   381  			Expect(response.Status()).To(Equal(http.StatusNoContent))
   382  			Expect(response.Body()).To(BeNil())
   383  		})
   384  
   385  		It("Sends impersonation header", func() {
   386  			// Configure the server:
   387  			apiServer.AppendHandlers(
   388  				ghttp.CombineHandlers(
   389  					ghttp.VerifyHeaderKV("Impersonate-User", "my-user"),
   390  					RespondWithJSON(http.StatusOK, "{}"),
   391  				),
   392  			)
   393  
   394  			// Prepare the body:
   395  			body, err := cmv1.NewCluster().
   396  				Name("my-cluster").
   397  				Build()
   398  			Expect(err).ToNot(HaveOccurred())
   399  
   400  			// Send the request:
   401  			_, err = connection.ClustersMgmt().V1().Clusters().Add().
   402  				Impersonate("my-user").
   403  				Body(body).
   404  				Send()
   405  			Expect(err).ToNot(HaveOccurred())
   406  		})
   407  	})
   408  
   409  	Describe("Patch", func() {
   410  		It("Accepts empty body", func() {
   411  			// Configure the server:
   412  			apiServer.AppendHandlers(
   413  				RespondWithJSON(http.StatusOK, ""),
   414  			)
   415  
   416  			// Send the request:
   417  			response, err := connection.Patch().
   418  				Path("/mypath").
   419  				Send()
   420  			Expect(err).ToNot(HaveOccurred())
   421  			Expect(response).ToNot(BeNil())
   422  			Expect(response.Status()).To(Equal(http.StatusOK))
   423  		})
   424  	})
   425  
   426  	Describe("Put", func() {
   427  		It("Accepts empty body", func() {
   428  			// Configure the server:
   429  			apiServer.AppendHandlers(
   430  				RespondWithJSON(http.StatusOK, ""),
   431  			)
   432  
   433  			// Send the request:
   434  			response, err := connection.Put().
   435  				Path("/mypath").
   436  				Send()
   437  			Expect(err).ToNot(HaveOccurred())
   438  			Expect(response).ToNot(BeNil())
   439  			Expect(response.Status()).To(Equal(http.StatusOK))
   440  		})
   441  	})
   442  
   443  	When("Server doesn't return JSON content type", func() {
   444  		It("It should ignore letter case", func() {
   445  			// Configure the server:
   446  			apiServer.AppendHandlers(
   447  				ghttp.RespondWith(
   448  					http.StatusOK, nil, http.Header{
   449  						"cOnTeNt-TyPe": []string{
   450  							"AppLicaTion/JSON",
   451  						},
   452  					},
   453  				),
   454  			)
   455  
   456  			// Send the request:
   457  			response, err := connection.Get().
   458  				Path("/api/clusters_mgmt/v1/clusters").
   459  				Send()
   460  			Expect(err).ToNot(HaveOccurred())
   461  			Expect(response).ToNot(BeNil())
   462  			Expect(response.Status()).To(Equal(http.StatusOK))
   463  		})
   464  
   465  		It("Adds complete content to error message if it is short", func() {
   466  			// Configure the server:
   467  			apiServer.AppendHandlers(
   468  				ghttp.RespondWith(
   469  					http.StatusBadGateway,
   470  					`Service not available`,
   471  					http.Header{
   472  						"Content-Type": []string{
   473  							"text/plain",
   474  						},
   475  					},
   476  				),
   477  			)
   478  
   479  			// Try to get the access token:
   480  			_, err := connection.Get().
   481  				Path("/api/clusters_mgmt/v1/clusters").
   482  				Send()
   483  			Expect(err).To(HaveOccurred())
   484  			message := err.Error()
   485  			Expect(message).To(ContainSubstring("text/plain"))
   486  			Expect(message).To(ContainSubstring("Service not available"))
   487  		})
   488  
   489  		It("Extracts and summarizes text if it's a long html", func() {
   490  			// Calculate a long message:
   491  			content := gatewayError
   492  
   493  			// Configure the server:
   494  			apiServer.AppendHandlers(
   495  				RespondWithContent(http.StatusBadGateway, "text/html", content),
   496  			)
   497  
   498  			// Try to get the access token:
   499  			_, err := connection.Get().
   500  				Path("/api/clusters_mgmt/v1/clusters").
   501  				Send()
   502  			Expect(err).To(HaveOccurred())
   503  			message := err.Error()
   504  			Expect(message).To(ContainSubstring("text/html"))
   505  			Expect(message).To(ContainSubstring("Application is not available"))
   506  			Expect(message).To(ContainSubstring("..."))
   507  		})
   508  
   509  		It("Summary shows html entities in a readable form", func() {
   510  			content := errorWithHTMLEntities
   511  
   512  			// Configure the server:
   513  			apiServer.AppendHandlers(
   514  				RespondWithContent(http.StatusBadGateway, "text/html", content),
   515  			)
   516  
   517  			// Try to get the access token:
   518  			_, err := connection.Get().
   519  				Path("/api/clusters_mgmt/v1/clusters").
   520  				Send()
   521  			Expect(err).To(HaveOccurred())
   522  			message := err.Error()
   523  			Expect(message).To(ContainSubstring("text/html"))
   524  			Expect(message).NotTo(ContainSubstring("tag was not removed"))
   525  			Expect(message).To(ContainSubstring(
   526  				`You don't have permission to access "http://sso.redhat.com/AK_PM_VPATH0/" ` +
   527  					`on this server. Reference #18.3500e8ac.1601993172.3a9c59e`))
   528  			Expect(message).To(ContainSubstring(`< > " & € ∭`))
   529  			// Sufficiently short to log Akamai reference number without shortening.
   530  			Expect(message).NotTo(ContainSubstring("..."))
   531  		})
   532  	})
   533  })
   534  
   535  const gatewayError = `
   536  <html>
   537    <head>
   538      <meta name="viewport" content="width=device-width, initial-scale=1">
   539  
   540    <style type="text/css">
   541    /*!
   542     * Bootstrap v3.3.5 (http://getbootstrap.com)
   543     * Copyright 2011-2015 Twitter, Inc.
   544     * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
   545     */
   546    /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
   547    html {
   548      font-family: sans-serif;
   549      -ms-text-size-adjust: 100%;
   550      -webkit-text-size-adjust: 100%;
   551    }
   552    body {
   553      margin: 0;
   554    }
   555    h1 {
   556      font-size: 1.7em;
   557      font-weight: 400;
   558      line-height: 1.3;
   559      margin: 0.68em 0;
   560    }
   561    * {
   562      -webkit-box-sizing: border-box;
   563      -moz-box-sizing: border-box;
   564      box-sizing: border-box;
   565    }
   566    *:before,
   567    *:after {
   568      -webkit-box-sizing: border-box;
   569      -moz-box-sizing: border-box;
   570      box-sizing: border-box;
   571    }
   572    html {
   573      -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
   574    }
   575    body {
   576      font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
   577      line-height: 1.66666667;
   578      font-size: 13px;
   579      color: #333333;
   580      background-color: #ffffff;
   581      margin: 2em 1em;
   582    }
   583    p {
   584      margin: 0 0 10px;
   585      font-size: 13px;
   586    }
   587    .alert.alert-info {
   588      padding: 15px;
   589      margin-bottom: 20px;
   590      border: 1px solid transparent;
   591      background-color: #f5f5f5;
   592      border-color: #8b8d8f;
   593      color: #363636;
   594      margin-top: 30px;
   595    }
   596    .alert p {
   597      padding-left: 35px;
   598    }
   599    a {
   600      color: #0088ce;
   601    }
   602  
   603    ul {
   604      position: relative;
   605      padding-left: 51px;
   606    }
   607    p.info {
   608      position: relative;
   609      font-size: 15px;
   610      margin-bottom: 10px;
   611    }
   612    p.info:before, p.info:after {
   613      content: "";
   614      position: absolute;
   615      top: 9%;
   616      left: 0;
   617    }
   618    p.info:before {
   619      content: "i";
   620      left: 3px;
   621      width: 20px;
   622      height: 20px;
   623      font-family: serif;
   624      font-size: 15px;
   625      font-weight: bold;
   626      line-height: 21px;
   627      text-align: center;
   628      color: #fff;
   629      background: #4d5258;
   630      border-radius: 16px;
   631    }
   632  
   633    @media (min-width: 768px) {
   634      body {
   635        margin: 4em 3em;
   636      }
   637      h1 {
   638        font-size: 2.15em;}
   639    }
   640  
   641    </style>
   642    </head>
   643    <body>
   644      <div>
   645        <h1>Application is not available</h1>
   646        <p>The application is currently not serving requests at this endpoint.
   647  		It may not have been started or is still starting.</p>
   648  
   649        <div class="alert alert-info">
   650          <p class="info">
   651            Possible reasons you are seeing this page:
   652          </p>
   653          <ul>
   654            <li>
   655              <strong>The host doesn't exist.</strong>
   656              Make sure the hostname was typed correctly and that a route matching this hostname exists.
   657            </li>
   658            <li>
   659              <strong>The host exists, but doesn't have a matching path.</strong>
   660              Check if the URL path was typed correctly and that the route was created using the desired path.
   661            </li>
   662            <li>
   663              <strong>Route and path matches, but all pods are down.</strong>
   664              Make sure that the resources exposed by this route (pods, services, deployment configs, etc)
   665  			have at least one pod running.
   666            </li>
   667          </ul>
   668        </div>
   669      </div>
   670    </body>
   671  </html>
   672  `
   673  
   674  // The text in body is a real response from Akamai blocking/rate-limiting our access to SSO.
   675  // I don't have the original HTML so the head & tags are made up.
   676  const errorWithHTMLEntities = `
   677  <html>
   678    <head>
   679    <title>Access Denied</title>
   680    <script>
   681     if(2 < 3) alert("2 &lt; 3 but more imporantly &lt;script&gt; tag was not removed!");
   682    </script>
   683    </head>
   684    <body>
   685     <h1>Access Denied</h1>
   686     <p>You don't have permission to access
   687     "http&#58;&#47;&#47;sso&#46;redhat&#46;com&#47;AK&#95;PM&#95;VPATH0&#47;" on this server.
   688     Reference&#32;&#35;18&#46;3500e8ac&#46;1601993172&#46;3a9c59e</p>
   689     &lt; &gt; &quot; &amp; &euro; &tint;
   690    </body>
   691  </html>
   692  `