|
38 | 38 | import com.cloudant.client.api.CloudantClient; |
39 | 39 | import com.cloudant.client.org.lightcouch.CouchDbException; |
40 | 40 | import com.cloudant.http.Http; |
| 41 | +import com.cloudant.http.interceptors.Replay429Interceptor; |
41 | 42 | import com.cloudant.tests.extensions.MockWebServerExtension; |
42 | 43 | import com.cloudant.tests.util.IamSystemPropertyMock; |
43 | 44 | import com.cloudant.tests.util.MockWebServerResources; |
@@ -322,7 +323,6 @@ public void iamRenewalFailureOnIamToken() throws Exception { |
322 | 323 | assertThrows(CouchDbException.class, |
323 | 324 | () -> c.executeRequest(Http.GET(c.getBaseUri())).responseAsString(), |
324 | 325 | "Failure to get a token should throw a CouchDbException."); |
325 | | - re.printStackTrace(); |
326 | 326 | assertTrue(re.getMessage().startsWith("HTTP response error getting session"), "The " + |
327 | 327 | "exception should have been for a HTTP response error."); |
328 | 328 |
|
@@ -458,5 +458,166 @@ public void iamRenewalFailureOnSessionCookie() throws Exception { |
458 | 458 | "/identity/token"); |
459 | 459 | } |
460 | 460 |
|
| 461 | + @Test |
| 462 | + public void iamTokenServer429RetryAndSucceed() throws Exception { |
| 463 | + // Mock request sequence |
| 464 | + mockIamServer.enqueue(new MockResponse().setResponseCode(200).setBody(IAM_TOKEN)); |
| 465 | + mockWebServer.enqueue(OK_IAM_COOKIE); |
| 466 | + // First get request succeeds |
| 467 | + mockWebServer.enqueue(new MockResponse().setResponseCode(200) |
| 468 | + .setBody(hello)); |
| 469 | + |
| 470 | + // Second get request has a 401 cookie expired |
| 471 | + mockWebServer.enqueue(new MockResponse().setResponseCode(401). |
| 472 | + setBody("{\"error\":\"credentials_expired\"}")); |
| 473 | + // IAM server 429 on token request |
| 474 | + mockIamServer.enqueue(new MockResponse().setStatus("HTTP/1.1 429 Too many requests")); |
| 475 | + // Success on retry |
| 476 | + mockIamServer.enqueue(new MockResponse().setResponseCode(200).setBody(IAM_TOKEN_2)); |
| 477 | + mockWebServer.enqueue(OK_IAM_COOKIE_2); |
| 478 | + // Second get request suceeds after renewal |
| 479 | + mockWebServer.enqueue(new MockResponse().setResponseCode(200) |
| 480 | + .setBody(hello)); |
| 481 | + |
| 482 | + // Request sequence |
| 483 | + CloudantClient c = CloudantClientHelper.newMockWebServerClientBuilder(mockWebServer) |
| 484 | + .iamApiKey(IAM_API_KEY) |
| 485 | + .interceptors(Replay429Interceptor.WITH_DEFAULTS) |
| 486 | + .build(); |
| 487 | + |
| 488 | + String response = c.executeRequest(Http.GET(c.getBaseUri())).responseAsString(); |
| 489 | + assertEquals(hello, response, "The expected response should be received"); |
| 490 | + |
| 491 | + String response2 = c.executeRequest(Http.GET(c.getBaseUri())).responseAsString(); |
| 492 | + assertEquals(hello, response2, "The expected response should be received"); |
| 493 | + |
| 494 | + // iam mock server |
| 495 | + |
| 496 | + // assert that there were 3 calls |
| 497 | + RecordedRequest[] recordedIamRequests = takeN(mockIamServer, 3); |
| 498 | + // first time, automatically fetch because cookie jar is empty |
| 499 | + assertEquals(iamTokenEndpoint, |
| 500 | + recordedIamRequests[0].getPath(), "The request should have been for " + |
| 501 | + "/identity/token"); |
| 502 | + assertThat("The request body should contain the IAM API key", |
| 503 | + recordedIamRequests[0].getBody().readString(Charset.forName("UTF-8")), |
| 504 | + containsString("apikey=" + IAM_API_KEY)); |
| 505 | + // second time, 429 response |
| 506 | + assertEquals(iamTokenEndpoint, |
| 507 | + recordedIamRequests[1].getPath(), "The request should have been for " + |
| 508 | + "/identity/token"); |
| 509 | + // third time, refresh because the cloudant session cookie has expired |
| 510 | + assertEquals(iamTokenEndpoint, |
| 511 | + recordedIamRequests[1].getPath(), "The request should have been for " + |
| 512 | + "/identity/token"); |
| 513 | + |
| 514 | + // cloudant mock server |
| 515 | + |
| 516 | + // assert that there were 5 calls |
| 517 | + RecordedRequest[] recordedRequests = takeN(mockWebServer, 5); |
| 518 | + |
| 519 | + assertEquals("/_iam_session", |
| 520 | + recordedRequests[0].getPath(), "The request should have been for /_iam_session"); |
| 521 | + assertThat("The request body should contain the IAM token", |
| 522 | + recordedRequests[0].getBody().readString(Charset.forName("UTF-8")), |
| 523 | + containsString(IAM_TOKEN)); |
| 524 | + // first request |
| 525 | + assertEquals("/", |
| 526 | + recordedRequests[1].getPath(), "The request should have been for /"); |
| 527 | + // The cookie may or may not have the session id quoted, so check both |
| 528 | + assertThat("The Cookie header should contain the expected session value", |
| 529 | + recordedRequests[1].getHeader("Cookie"), |
| 530 | + anyOf(containsString(iamSession(EXPECTED_OK_COOKIE)), |
| 531 | + containsString(iamSessionUnquoted(EXPECTED_OK_COOKIE)))); |
| 532 | + // second request (rejected for cookie expiry) |
| 533 | + assertEquals("/", |
| 534 | + recordedRequests[2].getPath(), "The request should have been for /"); |
| 535 | + // with new valid token get a new session |
| 536 | + assertEquals("/_iam_session", |
| 537 | + recordedRequests[3].getPath(), "The request should have been for /_iam_session"); |
| 538 | + assertThat("The request body should contain the IAM token", |
| 539 | + recordedRequests[3].getBody().readString(Charset.forName("UTF-8")), |
| 540 | + containsString(IAM_TOKEN_2)); |
| 541 | + assertEquals("/", |
| 542 | + recordedRequests[2].getPath(), "The request should have been for /"); |
| 543 | + } |
| 544 | + |
| 545 | + @Test |
| 546 | + public void iamTokenServer429RetryAndFail() throws Exception { |
| 547 | + // Mock request sequence |
| 548 | + mockIamServer.enqueue(new MockResponse().setResponseCode(200).setBody(IAM_TOKEN)); |
| 549 | + mockWebServer.enqueue(OK_IAM_COOKIE); |
| 550 | + // First get request succeeds |
| 551 | + mockWebServer.enqueue(new MockResponse().setResponseCode(200) |
| 552 | + .setBody(hello)); |
| 553 | + |
| 554 | + // Second get request has a 401 cookie expired |
| 555 | + mockWebServer.enqueue(new MockResponse().setResponseCode(401). |
| 556 | + setBody("{\"error\":\"credentials_expired\"}")); |
| 557 | + // IAM server 429 on subsequent token requests |
| 558 | + mockIamServer.enqueue(new MockResponse().setStatus("HTTP/1.1 429 Too many requests")); |
| 559 | + mockIamServer.enqueue(new MockResponse().setStatus("HTTP/1.1 429 Too many requests")); |
| 560 | + mockIamServer.enqueue(new MockResponse().setStatus("HTTP/1.1 429 Too many requests")); |
| 561 | + mockIamServer.enqueue(new MockResponse().setStatus("HTTP/1.1 429 Too many requests")); |
| 562 | + // Request will fail |
| 563 | + |
| 564 | + // Request sequence |
| 565 | + CloudantClient c = CloudantClientHelper.newMockWebServerClientBuilder(mockWebServer) |
| 566 | + .iamApiKey(IAM_API_KEY) |
| 567 | + .interceptors(Replay429Interceptor.WITH_DEFAULTS) |
| 568 | + .build(); |
461 | 569 |
|
| 570 | + String response = c.executeRequest(Http.GET(c.getBaseUri())).responseAsString(); |
| 571 | + assertEquals(hello, response, "The expected response should be received"); |
| 572 | + |
| 573 | + CouchDbException re = |
| 574 | + assertThrows(CouchDbException.class, |
| 575 | + () -> c.executeRequest(Http.GET(c.getBaseUri())).responseAsString(), |
| 576 | + "Failure to get a token should throw a CouchDbException."); |
| 577 | + assertTrue(re.getMessage().startsWith("HTTP response error getting session"), "The " + |
| 578 | + "exception should have been for a HTTP response error."); |
| 579 | + assertTrue(re.getMessage().contains("response code 429"), "The exception should report a " + |
| 580 | + "429 response code"); |
| 581 | + |
| 582 | + // iam mock server |
| 583 | + |
| 584 | + // assert that there were 5 calls |
| 585 | + RecordedRequest[] recordedIamRequests = takeN(mockIamServer, 5); |
| 586 | + // first time, automatically fetch because cookie jar is empty |
| 587 | + assertEquals(iamTokenEndpoint, |
| 588 | + recordedIamRequests[0].getPath(), "The request should have been for " + |
| 589 | + "/identity/token"); |
| 590 | + assertThat("The request body should contain the IAM API key", |
| 591 | + recordedIamRequests[0].getBody().readString(Charset.forName("UTF-8")), |
| 592 | + containsString("apikey=" + IAM_API_KEY)); |
| 593 | + // 4 more times all 429 responses |
| 594 | + for (int i = 1; i <= 4; i++) { |
| 595 | + assertEquals(iamTokenEndpoint, |
| 596 | + recordedIamRequests[i].getPath(), "The request[" + i + "] should have been " + |
| 597 | + "for " + |
| 598 | + "/identity/token"); |
| 599 | + } |
| 600 | + |
| 601 | + // cloudant mock server |
| 602 | + |
| 603 | + // assert that there were 3 calls |
| 604 | + RecordedRequest[] recordedRequests = takeN(mockWebServer, 3); |
| 605 | + |
| 606 | + assertEquals("/_iam_session", |
| 607 | + recordedRequests[0].getPath(), "The request should have been for /_iam_session"); |
| 608 | + assertThat("The request body should contain the IAM token", |
| 609 | + recordedRequests[0].getBody().readString(Charset.forName("UTF-8")), |
| 610 | + containsString(IAM_TOKEN)); |
| 611 | + // first request |
| 612 | + assertEquals("/", |
| 613 | + recordedRequests[1].getPath(), "The request should have been for /"); |
| 614 | + // The cookie may or may not have the session id quoted, so check both |
| 615 | + assertThat("The Cookie header should contain the expected session value", |
| 616 | + recordedRequests[1].getHeader("Cookie"), |
| 617 | + anyOf(containsString(iamSession(EXPECTED_OK_COOKIE)), |
| 618 | + containsString(iamSessionUnquoted(EXPECTED_OK_COOKIE)))); |
| 619 | + // Second request gives a 401 that starts token renewal |
| 620 | + // Retry of that request never reaches the server |
| 621 | + assertEquals("/", recordedRequests[1].getPath(), "The request should have been for /"); |
| 622 | + } |
462 | 623 | } |
0 commit comments