|
24 | 24 | import org.mockito.MockitoAnnotations; |
25 | 25 |
|
26 | 26 | import java.io.ByteArrayInputStream; |
| 27 | +import java.io.InputStream; |
27 | 28 | import java.nio.charset.StandardCharsets; |
28 | 29 | import java.util.Base64; |
29 | 30 |
|
@@ -634,7 +635,152 @@ void testFetchManifest_LayerMissingDigest() throws Exception { |
634 | 635 | testFetchManifestWithErrorExpected(invalidManifestJson, "layer missing required 'digest' field"); |
635 | 636 | } |
636 | 637 |
|
637 | | - // ========== Helper Methods ========== |
| 638 | + @Test |
| 639 | + void testDownloadBlob_Success() throws Exception { |
| 640 | + String blobContent = "test blob content"; |
| 641 | + String digest = "sha256:abc123"; |
| 642 | + |
| 643 | + CloseableHttpClient mockHttpClient = createMockHttpClientForBlob(blobContent, HttpStatus.SC_OK, true); |
| 644 | + |
| 645 | + try (MockedStatic<AuthChallenge> mockedAuthChallenge = mockAuthChallenge()) { |
| 646 | + OciRegistryClient client = new OciRegistryClient(REGISTRY_ENDPOINT, mockAuthProvider, mockHttpClient); |
| 647 | + |
| 648 | + try (InputStream stream = client.downloadBlob(REPOSITORY, digest)) { |
| 649 | + assertNotNull(stream); |
| 650 | + byte[] content = stream.readAllBytes(); |
| 651 | + assertEquals(blobContent, new String(content, StandardCharsets.UTF_8)); |
| 652 | + } |
| 653 | + |
| 654 | + client.close(); |
| 655 | + } |
| 656 | + } |
| 657 | + |
| 658 | + @Test |
| 659 | + void testDownloadBlob_HttpError_404_ThrowsResourceNotFoundException() throws Exception { |
| 660 | + String digest = "sha256:notfound"; |
| 661 | + |
| 662 | + CloseableHttpClient mockHttpClient = createMockHttpClientForBlob("Not Found", HttpStatus.SC_NOT_FOUND, true); |
| 663 | + |
| 664 | + try (MockedStatic<AuthChallenge> mockedAuthChallenge = mockAuthChallenge()) { |
| 665 | + OciRegistryClient client = new OciRegistryClient(REGISTRY_ENDPOINT, mockAuthProvider, mockHttpClient); |
| 666 | + |
| 667 | + ResourceNotFoundException exception = assertThrows(ResourceNotFoundException.class, |
| 668 | + () -> client.downloadBlob(REPOSITORY, digest)); |
| 669 | + assertTrue(exception.getMessage().contains("HTTP 404")); |
| 670 | + |
| 671 | + client.close(); |
| 672 | + } |
| 673 | + } |
| 674 | + |
| 675 | + @Test |
| 676 | + void testDownloadBlob_HttpError_401_ThrowsUnAuthorizedException() throws Exception { |
| 677 | + String digest = "sha256:unauthorized"; |
| 678 | + |
| 679 | + CloseableHttpClient mockHttpClient = createMockHttpClientForBlob("Unauthorized", HttpStatus.SC_UNAUTHORIZED, true); |
| 680 | + |
| 681 | + try (MockedStatic<AuthChallenge> mockedAuthChallenge = mockAuthChallenge()) { |
| 682 | + OciRegistryClient client = new OciRegistryClient(REGISTRY_ENDPOINT, mockAuthProvider, mockHttpClient); |
| 683 | + |
| 684 | + UnAuthorizedException exception = assertThrows(UnAuthorizedException.class, |
| 685 | + () -> client.downloadBlob(REPOSITORY, digest)); |
| 686 | + assertTrue(exception.getMessage().contains("HTTP 401")); |
| 687 | + |
| 688 | + client.close(); |
| 689 | + } |
| 690 | + } |
| 691 | + |
| 692 | + @Test |
| 693 | + void testDownloadBlob_HttpError_403_ThrowsUnAuthorizedException() throws Exception { |
| 694 | + String digest = "sha256:forbidden"; |
| 695 | + |
| 696 | + CloseableHttpClient mockHttpClient = createMockHttpClientForBlob("Forbidden", HttpStatus.SC_FORBIDDEN, true); |
| 697 | + |
| 698 | + try (MockedStatic<AuthChallenge> mockedAuthChallenge = mockAuthChallenge()) { |
| 699 | + OciRegistryClient client = new OciRegistryClient(REGISTRY_ENDPOINT, mockAuthProvider, mockHttpClient); |
| 700 | + |
| 701 | + UnAuthorizedException exception = assertThrows(UnAuthorizedException.class, |
| 702 | + () -> client.downloadBlob(REPOSITORY, digest)); |
| 703 | + assertTrue(exception.getMessage().contains("HTTP 403")); |
| 704 | + |
| 705 | + client.close(); |
| 706 | + } |
| 707 | + } |
| 708 | + |
| 709 | + @Test |
| 710 | + void testDownloadBlob_HttpError_500_ThrowsUnknownException() throws Exception { |
| 711 | + String digest = "sha256:servererror"; |
| 712 | + |
| 713 | + CloseableHttpClient mockHttpClient = createMockHttpClientForBlob("Internal Server Error", HttpStatus.SC_INTERNAL_SERVER_ERROR, true); |
| 714 | + |
| 715 | + try (MockedStatic<AuthChallenge> mockedAuthChallenge = mockAuthChallenge()) { |
| 716 | + OciRegistryClient client = new OciRegistryClient(REGISTRY_ENDPOINT, mockAuthProvider, mockHttpClient); |
| 717 | + |
| 718 | + UnknownException exception = assertThrows(UnknownException.class, |
| 719 | + () -> client.downloadBlob(REPOSITORY, digest)); |
| 720 | + assertTrue(exception.getMessage().contains("HTTP 500")); |
| 721 | + |
| 722 | + client.close(); |
| 723 | + } |
| 724 | + } |
| 725 | + |
| 726 | + @Test |
| 727 | + void testDownloadBlob_EmptyResponseBody_ThrowsUnknownException() throws Exception { |
| 728 | + String digest = "sha256:empty"; |
| 729 | + |
| 730 | + CloseableHttpClient mockHttpClient = createMockHttpClientForBlob(null, HttpStatus.SC_OK, false); |
| 731 | + |
| 732 | + try (MockedStatic<AuthChallenge> mockedAuthChallenge = mockAuthChallenge()) { |
| 733 | + OciRegistryClient client = new OciRegistryClient(REGISTRY_ENDPOINT, mockAuthProvider, mockHttpClient); |
| 734 | + |
| 735 | + UnknownException exception = assertThrows(UnknownException.class, |
| 736 | + () -> client.downloadBlob(REPOSITORY, digest)); |
| 737 | + assertTrue(exception.getMessage().contains("empty response body")); |
| 738 | + |
| 739 | + client.close(); |
| 740 | + } |
| 741 | + } |
| 742 | + |
| 743 | + @Test |
| 744 | + void testDownloadBlob_SetsAuthorizationHeader() throws Exception { |
| 745 | + String digest = "sha256:authtest"; |
| 746 | + CloseableHttpClient mockHttpClient = createMockHttpClientWithExecuteAnswer("auth test", invocation -> { |
| 747 | + HttpGet request = invocation.getArgument(0); |
| 748 | + Header authHeader = request.getFirstHeader("Authorization"); |
| 749 | + assertNotNull(authHeader, "Authorization header should be set"); |
| 750 | + assertEquals("Basic dXNlcjp0b2tlbg==", authHeader.getValue()); // Base64("user:token") |
| 751 | + }); |
| 752 | + |
| 753 | + try (MockedStatic<AuthChallenge> mockedAuthChallenge = mockAuthChallenge()) { |
| 754 | + OciRegistryClient client = new OciRegistryClient(REGISTRY_ENDPOINT, mockAuthProvider, mockHttpClient); |
| 755 | + try (InputStream stream = client.downloadBlob(REPOSITORY, digest)) { |
| 756 | + assertNotNull(stream); |
| 757 | + } |
| 758 | + client.close(); |
| 759 | + } |
| 760 | + } |
| 761 | + |
| 762 | + @Test |
| 763 | + void testDownloadBlob_DoesNotSetAuthHeaderForAnonymous() throws Exception { |
| 764 | + String digest = "sha256:noauthtest"; |
| 765 | + CloseableHttpClient mockHttpClient = createMockHttpClientWithExecuteAnswer("no auth test", invocation -> { |
| 766 | + HttpGet request = invocation.getArgument(0); |
| 767 | + Header authHeader = request.getFirstHeader("Authorization"); |
| 768 | + assertNull(authHeader, "Authorization header should not be set for anonymous auth"); |
| 769 | + }); |
| 770 | + |
| 771 | + AuthChallenge anonymousChallenge = AuthChallenge.anonymous(); |
| 772 | + try (MockedStatic<AuthChallenge> mockedAuthChallenge = mockStatic(AuthChallenge.class)) { |
| 773 | + mockedAuthChallenge.when(() -> AuthChallenge.discover(any(CloseableHttpClient.class), anyString())) |
| 774 | + .thenReturn(anonymousChallenge); |
| 775 | + mockedAuthChallenge.when(AuthChallenge::anonymous).thenCallRealMethod(); |
| 776 | + |
| 777 | + OciRegistryClient client = new OciRegistryClient(REGISTRY_ENDPOINT, mockAuthProvider, mockHttpClient); |
| 778 | + try (InputStream stream = client.downloadBlob(REPOSITORY, digest)) { |
| 779 | + assertNotNull(stream); |
| 780 | + } |
| 781 | + client.close(); |
| 782 | + } |
| 783 | + } |
638 | 784 |
|
639 | 785 | private void testFetchManifestWithResponse(String responseBody, String digestHeader, |
640 | 786 | int statusCode, ManifestAssertion assertion) throws Exception { |
@@ -729,6 +875,68 @@ private MockedStatic<AuthChallenge> mockAuthChallenge() { |
729 | 875 | return mockedAuthChallenge; |
730 | 876 | } |
731 | 877 |
|
| 878 | + /** |
| 879 | + * Creates a mock HttpClient that invokes a custom assertion on the outgoing request before returning a 200 response. |
| 880 | + * |
| 881 | + * @param blobContent the blob content to return |
| 882 | + * @param requestAssertion assertion to run on the outgoing HttpGet (e.g. to check headers) |
| 883 | + * @return mocked CloseableHttpClient |
| 884 | + */ |
| 885 | + private CloseableHttpClient createMockHttpClientWithExecuteAnswer(String blobContent, |
| 886 | + java.util.function.Consumer<org.mockito.invocation.InvocationOnMock> requestAssertion) { |
| 887 | + CloseableHttpClient mockHttpClient = mock(CloseableHttpClient.class); |
| 888 | + CloseableHttpResponse mockResponse = mock(CloseableHttpResponse.class); |
| 889 | + StatusLine mockStatusLine = mock(StatusLine.class); |
| 890 | + HttpEntity mockEntity = mock(HttpEntity.class); |
| 891 | + try { |
| 892 | + when(mockStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK); |
| 893 | + when(mockResponse.getStatusLine()).thenReturn(mockStatusLine); |
| 894 | + when(mockResponse.getEntity()).thenReturn(mockEntity); |
| 895 | + when(mockEntity.getContent()).thenReturn( |
| 896 | + new ByteArrayInputStream(blobContent.getBytes(StandardCharsets.UTF_8))); |
| 897 | + when(mockHttpClient.execute(any(HttpGet.class))).thenAnswer(invocation -> { |
| 898 | + requestAssertion.accept(invocation); |
| 899 | + return mockResponse; |
| 900 | + }); |
| 901 | + } catch (Exception e) { |
| 902 | + throw new RuntimeException("Failed to setup mock", e); |
| 903 | + } |
| 904 | + return mockHttpClient; |
| 905 | + } |
| 906 | + |
| 907 | + /** |
| 908 | + * Creates a mock HttpClient for blob download tests. |
| 909 | + * |
| 910 | + * @param blobContent the blob content to return (null for no entity) |
| 911 | + * @param statusCode the HTTP status code |
| 912 | + * @param hasEntity whether the response has an entity |
| 913 | + * @return mocked CloseableHttpClient |
| 914 | + */ |
| 915 | + private CloseableHttpClient createMockHttpClientForBlob(String blobContent, int statusCode, boolean hasEntity) { |
| 916 | + CloseableHttpClient mockHttpClient = mock(CloseableHttpClient.class); |
| 917 | + CloseableHttpResponse mockResponse = mock(CloseableHttpResponse.class); |
| 918 | + StatusLine mockStatusLine = mock(StatusLine.class); |
| 919 | + HttpEntity mockEntity = hasEntity ? mock(HttpEntity.class) : null; |
| 920 | + |
| 921 | + try { |
| 922 | + when(mockStatusLine.getStatusCode()).thenReturn(statusCode); |
| 923 | + when(mockResponse.getStatusLine()).thenReturn(mockStatusLine); |
| 924 | + when(mockResponse.getEntity()).thenReturn(mockEntity); |
| 925 | + |
| 926 | + if (hasEntity && blobContent != null) { |
| 927 | + // Create a fresh InputStream each time getContent() is called |
| 928 | + when(mockEntity.getContent()).thenAnswer(invocation -> |
| 929 | + new ByteArrayInputStream(blobContent.getBytes(StandardCharsets.UTF_8))); |
| 930 | + } |
| 931 | + |
| 932 | + when(mockHttpClient.execute(any(HttpGet.class))).thenReturn(mockResponse); |
| 933 | + } catch (Exception e) { |
| 934 | + throw new RuntimeException("Failed to setup blob mocks", e); |
| 935 | + } |
| 936 | + |
| 937 | + return mockHttpClient; |
| 938 | + } |
| 939 | + |
732 | 940 | @FunctionalInterface |
733 | 941 | interface ManifestAssertion { |
734 | 942 | void assertManifest(com.salesforce.multicloudj.registry.model.Manifest manifest); |
|
0 commit comments