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 < 3 but more imporantly <script> 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://sso.redhat.com/AK_PM_VPATH0/" on this server. 688 Reference #18.3500e8ac.1601993172.3a9c59e</p> 689 < > " & € ∭ 690 </body> 691 </html> 692 `