diff --git a/CHANGELOG.md b/CHANGELOG.md index 3054a1b5a..e35e6b310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Map `XXX` error status code range to Parsable Exception object if more specific error status code range is not found. + ## [0.12.2] - 2024-02-01 ### Changed diff --git a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java index 72376e810..8b8069297 100644 --- a/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java +++ b/components/http/okHttp/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java @@ -620,7 +620,8 @@ private Response throwIfFailedResponse( && errorMappings.containsKey("4XX")) && !(statusCode >= 500 && statusCode < 600 - && errorMappings.containsKey("5XX"))) { + && errorMappings.containsKey("5XX")) + && !errorMappings.containsKey("XXX")) { spanForAttributes.setAttribute(errorMappingFoundAttributeName, false); final ApiException result = new ApiExceptionBuilder() @@ -640,8 +641,8 @@ private Response throwIfFailedResponse( errorMappings.containsKey(statusCodeAsString) ? errorMappings.get(statusCodeAsString) : (statusCode >= 400 && statusCode < 500 - ? errorMappings.get("4XX") - : errorMappings.get("5XX")); + ? errorMappings.getOrDefault("4XX", errorMappings.get("XXX")) + : errorMappings.getOrDefault("5XX", errorMappings.get("XXX"))); boolean closeResponse = true; try { final ParseNode rootNode = getRootParseNode(response, span, span); diff --git a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java index f47035074..227ff07c1 100644 --- a/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java +++ b/components/http/okHttp/src/test/java/com/microsoft/kiota/http/OkHttpRequestAdapterTest.java @@ -28,9 +28,10 @@ import okio.Okio; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.stubbing.Answer; @@ -39,15 +40,18 @@ import java.io.InputStream; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Stream; public class OkHttpRequestAdapterTest { @ParameterizedTest @EnumSource( value = HttpMethod.class, names = {"PUT", "POST", "PATCH"}) - public void PostRequestsShouldHaveEmptyBody(HttpMethod method) + void postRequestsShouldHaveEmptyBody(HttpMethod method) throws Exception { // Unexpected exception thrown: java.lang.IllegalArgumentException: // method POST must have a request body. final AuthenticationProvider authenticationProviderMock = @@ -70,7 +74,7 @@ public Request test() throws Exception { @ParameterizedTest @ValueSource(ints = {200, 201, 202, 203, 206}) - void SendStreamReturnsUsableStream(int statusCode) throws Exception { + void sendStreamReturnsUsableStream(int statusCode) throws Exception { final var authenticationProviderMock = mock(AuthenticationProvider.class); authenticationProviderMock.authenticateRequest( any(RequestInformation.class), any(Map.class)); @@ -113,7 +117,7 @@ void SendStreamReturnsUsableStream(int statusCode) throws Exception { @ParameterizedTest @ValueSource(ints = {200, 201, 202, 203, 204}) - public void SendStreamReturnsNullOnNoContent(int statusCode) throws Exception { + void sendStreamReturnsNullOnNoContent(int statusCode) throws Exception { final var authenticationProviderMock = mock(AuthenticationProvider.class); authenticationProviderMock.authenticateRequest( any(RequestInformation.class), any(Map.class)); @@ -142,7 +146,7 @@ public void SendStreamReturnsNullOnNoContent(int statusCode) throws Exception { @ParameterizedTest @ValueSource(ints = {200, 201, 202, 203, 204, 205}) - public void SendReturnsNullOnNoContent(int statusCode) throws Exception { + void sendReturnsNullOnNoContent(int statusCode) throws Exception { final var authenticationProviderMock = mock(AuthenticationProvider.class); authenticationProviderMock.authenticateRequest( any(RequestInformation.class), any(Map.class)); @@ -172,7 +176,7 @@ public void SendReturnsNullOnNoContent(int statusCode) throws Exception { @ParameterizedTest @ValueSource(ints = {200, 201, 202, 203}) - public void SendReturnsObjectOnContent(int statusCode) throws Exception { + void sendReturnsObjectOnContent(int statusCode) throws Exception { final var authenticationProviderMock = mock(AuthenticationProvider.class); authenticationProviderMock.authenticateRequest( any(RequestInformation.class), any(Map.class)); @@ -209,15 +213,38 @@ public void SendReturnsObjectOnContent(int statusCode) throws Exception { assertNotNull(response); } - @Test - public void throwsAPIException() throws Exception { + private static Stream providesErrorMappings() { + return Stream.of( + // unexpected error code exception + Arguments.of(404, null, false), + Arguments.of(400, Arrays.asList("5XX"), false), + Arguments.of(503, null, false), + Arguments.of(502, Arrays.asList("4XX"), false), + Arguments.of(502, Arrays.asList(""), false), + // expect deserialized exception + Arguments.of(404, Arrays.asList("404"), true), + Arguments.of(500, Arrays.asList("500"), true), + Arguments.of(404, Arrays.asList("XXX"), true), + Arguments.of(500, Arrays.asList("XXX"), true), + Arguments.of(404, Arrays.asList("5XX", "XXX"), true), + Arguments.of(500, Arrays.asList("4XX", "XXX"), true)); + } + + @SuppressWarnings("unchecked") + @ParameterizedTest + @MethodSource("providesErrorMappings") + void throwsAPIException( + int responseStatusCode, + List errorMappingCodes, + boolean expectDeserializedException) + throws Exception { final var authenticationProviderMock = mock(AuthenticationProvider.class); authenticationProviderMock.authenticateRequest( any(RequestInformation.class), any(Map.class)); final var client = getMockClient( new Response.Builder() - .code(404) + .code(responseStatusCode) .message("Not Found") .protocol(Protocol.HTTP_1_1) .request(new Request.Builder().url("http://localhost").build()) @@ -236,20 +263,33 @@ public void throwsAPIException() throws Exception { }; final var mockEntity = mock(Parsable.class); when(mockEntity.getFieldDeserializers()).thenReturn(new HashMap<>()); + final var mockParsableFactory = mock(ParsableFactory.class); + when(mockParsableFactory.create(any(ParseNode.class))).thenReturn(mockEntity); final var mockParseNode = mock(ParseNode.class); when(mockParseNode.getObjectValue(any(ParsableFactory.class))).thenReturn(mockEntity); final var mockFactory = mock(ParseNodeFactory.class); when(mockFactory.getParseNode(any(String.class), any(InputStream.class))) .thenReturn(mockParseNode); when(mockFactory.getValidContentType()).thenReturn("application/json"); + final var requestAdapter = new OkHttpRequestAdapter(authenticationProviderMock, mockFactory, null, client); + final var errorMappings = + errorMappingCodes == null + ? null + : new HashMap>(); + if (errorMappings != null) + errorMappingCodes.forEach((mapping) -> errorMappings.put(mapping, mockParsableFactory)); final var exception = assertThrows( ApiException.class, - () -> requestAdapter.send(requestInformation, null, (node) -> mockEntity)); + () -> + requestAdapter.send( + requestInformation, errorMappings, (node) -> mockEntity)); assertNotNull(exception); - assertEquals(404, exception.getResponseStatusCode()); + if (expectDeserializedException) + verify(mockParseNode, times(1)).getObjectValue(mockParsableFactory); + assertEquals(responseStatusCode, exception.getResponseStatusCode()); assertTrue(exception.getResponseHeaders().containsKey("request-id")); }