diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java deleted file mode 100644 index 46894323847521..00000000000000 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java +++ /dev/null @@ -1,635 +0,0 @@ -/* - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -package com.facebook.react.modules.network; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.CatalystInstance; -import com.facebook.react.bridge.JavaOnlyArray; -import com.facebook.react.bridge.JavaOnlyMap; -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.WritableArray; -import com.facebook.react.common.network.OkHttpCallUtil; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; -import okhttp3.Call; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.MultipartBody; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okio.Buffer; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.MockedStatic; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.robolectric.RobolectricTestRunner; - -/** Tests for {@link NetworkingModule}. */ -@RunWith(RobolectricTestRunner.class) -@Ignore("Ignored due to unsupported mocking mechanism with JDK 18") -public class NetworkingModuleTest { - private NetworkingModule mNetworkingModule; - private OkHttpClient mHttpClient; - private ReactApplicationContext mContext; - - private MockedStatic arguments; - - @Before - public void prepareModules() { - mHttpClient = mock(OkHttpClient.class); - when(mHttpClient.cookieJar()).thenReturn(mock(CookieJarContainer.class)); - when(mHttpClient.newCall(any(Request.class))) - .thenAnswer( - invocation -> { - Call callMock = mock(Call.class); - return callMock; - }); - OkHttpClient.Builder clientBuilder = mock(OkHttpClient.Builder.class); - when(clientBuilder.build()).thenReturn(mHttpClient); - when(mHttpClient.newBuilder()).thenReturn(clientBuilder); - - CatalystInstance reactInstance = mock(CatalystInstance.class); - - mContext = mock(ReactApplicationContext.class); - when(mContext.getCatalystInstance()).thenReturn(reactInstance); - when(mContext.hasActiveReactInstance()).thenReturn(true); - mNetworkingModule = new NetworkingModule(mContext, "", mHttpClient); - - arguments = mockStatic(Arguments.class); - arguments.when(Arguments::createArray).thenAnswer(invocation -> new JavaOnlyArray()); - arguments.when(Arguments::createMap).thenAnswer(invocation -> new JavaOnlyMap()); - } - - @After - public void tearDown() { - arguments.close(); - } - - @Test - public void testGetWithoutHeaders() throws Exception { - mNetworkingModule.sendRequest( - "GET", - "http://somedomain/foo", - /* requestId */ 0, - /* headers */ JavaOnlyArray.of(), - /* body */ null, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); - verify(mHttpClient).newCall(argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().url().toString()).isEqualTo("http://somedomain/foo"); - // We set the User-Agent header by default - assertThat(argumentCaptor.getValue().headers().size()).isEqualTo(1); - assertThat(argumentCaptor.getValue().method()).isEqualTo("GET"); - } - - @Test - public void testFailGetWithInvalidHeadersStruct() throws Exception { - List invalidHeaders = Arrays.asList(JavaOnlyArray.of("foo")); - - mNetworkingModule.sendRequest( - "GET", - "http://somedoman/foo", - /* requestId */ 0, - /* headers */ JavaOnlyArray.from(invalidHeaders), - /* body */ null, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - - verifyErrorEmit(mContext, 0); - } - - @Test - public void testFailPostWithoutContentType() throws Exception { - JavaOnlyMap body = new JavaOnlyMap(); - body.putString("string", "This is request body"); - - mNetworkingModule.sendRequest( - "POST", - "http://somedomain/bar", - 0, - JavaOnlyArray.of(), - body, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - - verifyErrorEmit(mContext, 0); - } - - @Test - public void testFailInvalidUrl() throws Exception { - mNetworkingModule.sendRequest( - "GET", - "aaa", - /* requestId */ 0, - /* headers */ JavaOnlyArray.of(), - /* body */ null, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - - verifyErrorEmit(mContext, 0); - } - - private static void verifyErrorEmit(ReactApplicationContext context, int requestId) { - ArgumentCaptor captor = ArgumentCaptor.forClass(WritableArray.class); - verify(context).emitDeviceEvent(eq("didCompleteNetworkResponse"), captor.capture()); - - WritableArray array = captor.getValue(); - assertThat(array.getInt(0)).isEqualTo(requestId); - assertThat(array.getString(1)).isNotNull(); - } - - @Test - public void testSuccessfulPostRequest() throws Exception { - JavaOnlyMap body = new JavaOnlyMap(); - body.putString("string", "This is request body"); - - mNetworkingModule.sendRequest( - "POST", - "http://somedomain/bar", - 0, - JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "text/plain")), - body, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); - verify(mHttpClient).newCall(argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().url().toString()).isEqualTo("http://somedomain/bar"); - assertThat(argumentCaptor.getValue().headers().size()).isEqualTo(2); - assertThat(argumentCaptor.getValue().method()).isEqualTo("POST"); - assertThat(argumentCaptor.getValue().body().contentType().type()).isEqualTo("text"); - assertThat(argumentCaptor.getValue().body().contentType().subtype()).isEqualTo("plain"); - Buffer contentBuffer = new Buffer(); - argumentCaptor.getValue().body().writeTo(contentBuffer); - assertThat(contentBuffer.readUtf8()).isEqualTo("This is request body"); - } - - @Test - public void testHeaders() throws Exception { - List headers = - Arrays.asList( - JavaOnlyArray.of("Accept", "text/plain"), - JavaOnlyArray.of("User-Agent", "React test agent/1.0")); - - mNetworkingModule.sendRequest( - "GET", - "http://someurl/baz", - 0, - JavaOnlyArray.from(headers), - null, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); - verify(mHttpClient).newCall(argumentCaptor.capture()); - Headers requestHeaders = argumentCaptor.getValue().headers(); - assertThat(requestHeaders.size()).isEqualTo(2); - assertThat(requestHeaders.get("Accept")).isEqualTo("text/plain"); - assertThat(requestHeaders.get("User-Agent")).isEqualTo("React test agent/1.0"); - } - - @Test - public void testPostJsonContentTypeHeader() throws Exception { - - JavaOnlyMap body = new JavaOnlyMap(); - body.putString("string", "{ \"key\": \"value\" }"); - - mNetworkingModule.sendRequest( - "POST", - "http://somedomain/bar", - 0, - JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "application/json")), - body, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); - verify(mHttpClient).newCall(argumentCaptor.capture()); - - // Verify okhttp does not append "charset=utf-8" - assertThat(argumentCaptor.getValue().body().contentType().toString()) - .isEqualTo("application/json"); - } - - @Test - public void testRespectsExistingCharacterSet() throws Exception { - JavaOnlyMap body = new JavaOnlyMap(); - body.putString("string", "Friðjónsson"); - - mNetworkingModule.sendRequest( - "POST", - "http://somedomain/bar", - 0, - JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "text/plain; charset=utf-16")), - body, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); - verify(mHttpClient).newCall(argumentCaptor.capture()); - - Buffer contentBuffer = new Buffer(); - argumentCaptor.getValue().body().writeTo(contentBuffer); - assertThat(contentBuffer.readString(StandardCharsets.UTF_16)).isEqualTo("Friðjónsson"); - } - - @Test - public void testGracefullyRecoversFromInvalidContentType() throws Exception { - JavaOnlyMap body = new JavaOnlyMap(); - body.putString("string", "test"); - - mNetworkingModule.sendRequest( - "POST", - "http://somedomain/bar", - 0, - JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "invalid")), - body, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); - verify(mHttpClient).newCall(argumentCaptor.capture()); - - Buffer contentBuffer = new Buffer(); - argumentCaptor.getValue().body().writeTo(contentBuffer); - - assertThat(contentBuffer.readString(StandardCharsets.UTF_8)).isEqualTo("test"); - assertThat(argumentCaptor.getValue().header("Content-Type")).isEqualTo("invalid"); - } - - @Test - public void testMultipartPostRequestSimple() throws Exception { - MockedStatic requestBodyUtil = mockStatic(RequestBodyUtil.class); - requestBodyUtil - .when(() -> RequestBodyUtil.getFileInputStream(any(ReactContext.class), any(String.class))) - .thenReturn(mock(InputStream.class)); - requestBodyUtil - .when(() -> RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))) - .thenReturn(mock(RequestBody.class)); - requestBodyUtil - .when( - () -> - RequestBodyUtil.createProgressRequest( - any(RequestBody.class), any(ProgressListener.class))) - .thenCallRealMethod(); - - JavaOnlyMap body = new JavaOnlyMap(); - JavaOnlyArray formData = new JavaOnlyArray(); - JavaOnlyMap bodyPart = new JavaOnlyMap(); - bodyPart.putString("string", "value"); - bodyPart.putArray( - "headers", - JavaOnlyArray.from(Arrays.asList(JavaOnlyArray.of("content-disposition", "name")))); - formData.pushMap(bodyPart); - body.putArray("formData", formData); - - mNetworkingModule.sendRequest( - "POST", - "http://someurl/uploadFoo", - 0, - new JavaOnlyArray(), - body, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - - // verify url, method, headers - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); - verify(mHttpClient).newCall(argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().url().toString()).isEqualTo("http://someurl/uploadFoo"); - assertThat(argumentCaptor.getValue().method()).isEqualTo("POST"); - assertThat(argumentCaptor.getValue().body().contentType().type()) - .isEqualTo(MultipartBody.FORM.type()); - assertThat(argumentCaptor.getValue().body().contentType().subtype()) - .isEqualTo(MultipartBody.FORM.subtype()); - Headers requestHeaders = argumentCaptor.getValue().headers(); - assertThat(requestHeaders.size()).isEqualTo(1); - - requestBodyUtil.close(); - } - - @Test - public void testMultipartPostRequestHeaders() throws Exception { - MockedStatic requestBodyUtil = mockStatic(RequestBodyUtil.class); - requestBodyUtil - .when(() -> RequestBodyUtil.getFileInputStream(any(ReactContext.class), any(String.class))) - .thenReturn(mock(InputStream.class)); - requestBodyUtil - .when(() -> RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))) - .thenReturn(mock(RequestBody.class)); - requestBodyUtil - .when( - () -> - RequestBodyUtil.createProgressRequest( - any(RequestBody.class), any(ProgressListener.class))) - .thenCallRealMethod(); - - List headers = - Arrays.asList( - JavaOnlyArray.of("Accept", "text/plain"), - JavaOnlyArray.of("User-Agent", "React test agent/1.0"), - JavaOnlyArray.of("content-type", "multipart/form-data")); - - JavaOnlyMap body = new JavaOnlyMap(); - JavaOnlyArray formData = new JavaOnlyArray(); - JavaOnlyMap bodyPart = new JavaOnlyMap(); - bodyPart.putString("string", "value"); - bodyPart.putArray( - "headers", - JavaOnlyArray.from(Arrays.asList(JavaOnlyArray.of("content-disposition", "name")))); - formData.pushMap(bodyPart); - body.putArray("formData", formData); - - mNetworkingModule.sendRequest( - "POST", - "http://someurl/uploadFoo", - 0, - JavaOnlyArray.from(headers), - body, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - - // verify url, method, headers - ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); - verify(mHttpClient).newCall(argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().url().toString()).isEqualTo("http://someurl/uploadFoo"); - assertThat(argumentCaptor.getValue().method()).isEqualTo("POST"); - assertThat(argumentCaptor.getValue().body().contentType().type()) - .isEqualTo(MultipartBody.FORM.type()); - assertThat(argumentCaptor.getValue().body().contentType().subtype()) - .isEqualTo(MultipartBody.FORM.subtype()); - Headers requestHeaders = argumentCaptor.getValue().headers(); - assertThat(requestHeaders.size()).isEqualTo(3); - assertThat(requestHeaders.get("Accept")).isEqualTo("text/plain"); - assertThat(requestHeaders.get("User-Agent")).isEqualTo("React test agent/1.0"); - assertThat(requestHeaders.get("content-type")).isEqualTo("multipart/form-data"); - } - - @Test - public void testMultipartPostRequestBody() throws Exception { - InputStream inputStream = mock(InputStream.class); - - MockedStatic requestBodyUtil = mockStatic(RequestBodyUtil.class); - requestBodyUtil - .when(() -> RequestBodyUtil.getFileInputStream(any(ReactContext.class), any(String.class))) - .thenReturn(mock(InputStream.class)); - requestBodyUtil - .when(() -> RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))) - .thenCallRealMethod(); - requestBodyUtil - .when( - () -> - RequestBodyUtil.createProgressRequest( - any(RequestBody.class), any(ProgressListener.class))) - .thenCallRealMethod(); - - when(inputStream.available()).thenReturn("imageUri".length()); - - final MultipartBody.Builder multipartBuilder = mock(MultipartBody.Builder.class); - // TODO This PowerMock statement should be migrated to an equivalent for Mockito - // once this test is unsuppressed. - // whenNew(MultipartBody.Builder.class) - // .withNoArguments() - // .thenReturn(multipartBuilder); - when(multipartBuilder.setType(any(MediaType.class))) - .thenAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - return multipartBuilder; - } - }); - when(multipartBuilder.addPart(any(Headers.class), any(RequestBody.class))) - .thenAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - return multipartBuilder; - } - }); - when(multipartBuilder.build()) - .thenAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - return mock(MultipartBody.class); - } - }); - - List headers = - Arrays.asList(JavaOnlyArray.of("content-type", "multipart/form-data")); - - JavaOnlyMap body = new JavaOnlyMap(); - JavaOnlyArray formData = new JavaOnlyArray(); - body.putArray("formData", formData); - - JavaOnlyMap bodyPart = new JavaOnlyMap(); - bodyPart.putString("string", "locale"); - bodyPart.putArray( - "headers", - JavaOnlyArray.from(Arrays.asList(JavaOnlyArray.of("content-disposition", "user")))); - formData.pushMap(bodyPart); - - JavaOnlyMap imageBodyPart = new JavaOnlyMap(); - imageBodyPart.putString("uri", "imageUri"); - imageBodyPart.putArray( - "headers", - JavaOnlyArray.from( - Arrays.asList( - JavaOnlyArray.of("content-type", "image/jpg"), - JavaOnlyArray.of( - "content-disposition", - "filename=\"测试photo.jpg\"; filename*=utf-8''%E6%B5%8B%E8%AF%95photo.jpg")))); - formData.pushMap(imageBodyPart); - - mNetworkingModule.sendRequest( - "POST", - "http://someurl/uploadFoo", - 0, - JavaOnlyArray.from(headers), - body, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - - // verify RequestBodyPart for image - - // TODO This should be migrated to requestBodyUtil.verify(); - // PowerMockito.verifyStatic(RequestBodyUtil.class, times(1)); - RequestBodyUtil.getFileInputStream(any(ReactContext.class), eq("imageUri")); - // TODO This should be migrated to requestBodyUtil.verify(); - // PowerMockito.verifyStatic(RequestBodyUtil.class, times(1)); - RequestBodyUtil.create(MediaType.parse("image/jpg"), inputStream); - - // verify body - verify(multipartBuilder).build(); - verify(multipartBuilder).setType(MultipartBody.FORM); - ArgumentCaptor headersArgumentCaptor = ArgumentCaptor.forClass(Headers.class); - ArgumentCaptor bodyArgumentCaptor = ArgumentCaptor.forClass(RequestBody.class); - verify(multipartBuilder, times(2)) - .addPart(headersArgumentCaptor.capture(), bodyArgumentCaptor.capture()); - - List bodyHeaders = headersArgumentCaptor.getAllValues(); - assertThat(bodyHeaders.size()).isEqualTo(2); - List bodyRequestBody = bodyArgumentCaptor.getAllValues(); - assertThat(bodyRequestBody.size()).isEqualTo(2); - - assertThat(bodyHeaders.get(0).get("content-disposition")).isEqualTo("user"); - assertThat(bodyRequestBody.get(0).contentType()).isNull(); - assertThat(bodyRequestBody.get(0).contentLength()).isEqualTo("locale".getBytes().length); - assertThat(bodyHeaders.get(1).get("content-disposition")) - .isEqualTo("filename=\"测试photo.jpg\"; filename*=utf-8''%E6%B5%8B%E8%AF%95photo.jpg"); - assertThat(bodyRequestBody.get(1).contentType()).isEqualTo(MediaType.parse("image/jpg")); - assertThat(bodyRequestBody.get(1).contentLength()).isEqualTo("imageUri".getBytes().length); - } - - @Test - public void testCancelAllCallsInvalidate() throws Exception { - MockedStatic okHttpCallUtil = mockStatic(OkHttpCallUtil.class); - final int requests = 3; - final Call[] calls = new Call[requests]; - for (int idx = 0; idx < requests; idx++) { - calls[idx] = mock(Call.class); - } - - when(mHttpClient.newCall(any(Request.class))) - .thenAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - Request request = (Request) invocation.getArguments()[0]; - return calls[(Integer) request.tag() - 1]; - } - }); - mNetworkingModule.initialize(); - - for (int idx = 0; idx < requests; idx++) { - mNetworkingModule.sendRequest( - "GET", - "http://somedomain/foo", - idx + 1, - JavaOnlyArray.of(), - null, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - } - verify(mHttpClient, times(3)).newCall(any(Request.class)); - - mNetworkingModule.invalidate(); - // TODO This should be migrated to okHttpCallUtil.verify(); - // PowerMockito.verifyStatic(OkHttpCallUtil.class, times(3)); - ArgumentCaptor clientArguments = ArgumentCaptor.forClass(OkHttpClient.class); - ArgumentCaptor requestIdArguments = ArgumentCaptor.forClass(Integer.class); - OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()); - - assertThat(requestIdArguments.getAllValues().size()).isEqualTo(requests); - for (int idx = 0; idx < requests; idx++) { - assertThat(requestIdArguments.getAllValues().contains(idx + 1)).isTrue(); - } - } - - @Test - public void testCancelSomeCallsInvalidate() throws Exception { - MockedStatic okHttpCallUtil = mockStatic(OkHttpCallUtil.class); - final int requests = 3; - final Call[] calls = new Call[requests]; - for (int idx = 0; idx < requests; idx++) { - calls[idx] = mock(Call.class); - } - - when(mHttpClient.newCall(any(Request.class))) - .thenAnswer( - new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - Request request = (Request) invocation.getArguments()[0]; - return calls[(Integer) request.tag() - 1]; - } - }); - - for (int idx = 0; idx < requests; idx++) { - mNetworkingModule.sendRequest( - "GET", - "http://somedomain/foo", - idx + 1, - JavaOnlyArray.of(), - null, - /* responseType */ "text", - /* useIncrementalUpdates*/ true, - /* timeout */ 0, - /* withCredentials */ false); - } - verify(mHttpClient, times(3)).newCall(any(Request.class)); - - mNetworkingModule.abortRequest(requests); - // TODO This should be migrated to okHttpCallUtil.verify(); - // PowerMockito.verifyStatic(OkHttpCallUtil.class, times(1)); - ArgumentCaptor clientArguments = ArgumentCaptor.forClass(OkHttpClient.class); - ArgumentCaptor requestIdArguments = ArgumentCaptor.forClass(Integer.class); - OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()); - assertThat(requestIdArguments.getAllValues().size()).isEqualTo(1); - assertThat(requestIdArguments.getAllValues().get(0)).isEqualTo(requests); - - // verifyStatic actually does not clear all calls so far, so we have to check for all of them. - // If `cancelTag` would've been called again for the aborted call, we would have had - // `requests + 1` calls. - mNetworkingModule.invalidate(); - // TODO This should be migrated to okHttpCallUtil.verify(); - // PowerMockito.verifyStatic(OkHttpCallUtil.class, times(requests)); - clientArguments = ArgumentCaptor.forClass(OkHttpClient.class); - requestIdArguments = ArgumentCaptor.forClass(Integer.class); - OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()); - assertThat(requestIdArguments.getAllValues().size()).isEqualTo(requests); - for (int idx = 0; idx < requests; idx++) { - assertThat(requestIdArguments.getAllValues().contains(idx + 1)).isTrue(); - } - } -} diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt new file mode 100644 index 00000000000000..89f7ffd98ed951 --- /dev/null +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.kt @@ -0,0 +1,599 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.modules.network + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.CatalystInstance +import com.facebook.react.bridge.JavaOnlyArray +import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.common.network.OkHttpCallUtil +import java.io.InputStream +import java.nio.charset.StandardCharsets +import okhttp3.Call +import okhttp3.Headers +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody +import okio.Buffer +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq +import org.mockito.Captor +import org.mockito.MockedStatic +import org.mockito.Mockito.mock +import org.mockito.Mockito.mockStatic +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.robolectric.RobolectricTestRunner + +/** + * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when null is + * returned. + */ +private fun anyOrNull(type: Class): T = any(type) + +/** + * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + */ +fun capture(argumentCaptor: ArgumentCaptor): T = argumentCaptor.capture() + +@RunWith(RobolectricTestRunner::class) +class NetworkingModuleTest { + private lateinit var networkingModule: NetworkingModule + private lateinit var httpClient: OkHttpClient + private lateinit var context: ReactApplicationContext + private lateinit var arguments: MockedStatic + private lateinit var requestBodyUtil: MockedStatic + + @Captor private lateinit var requestArgumentCaptor: ArgumentCaptor + + @Before + fun prepareModules() { + httpClient = mock(OkHttpClient::class.java) + whenever(httpClient.cookieJar()).thenReturn(mock(CookieJarContainer::class.java)) + whenever(httpClient.newCall(anyOrNull(Request::class.java))).thenAnswer { + val callMock = mock(Call::class.java) + callMock + } + + val clientBuilder = mock(OkHttpClient.Builder::class.java) + whenever(clientBuilder.build()).thenReturn(httpClient) + whenever(httpClient.newBuilder()).thenReturn(clientBuilder) + + val reactInstance = mock(CatalystInstance::class.java) + + context = mock(ReactApplicationContext::class.java) + whenever(context.catalystInstance).thenReturn(reactInstance) + whenever(context.hasActiveReactInstance()).thenReturn(true) + + networkingModule = NetworkingModule(context, "", httpClient) + + arguments = mockStatic(Arguments::class.java) + arguments.`when`(Arguments::createArray).thenAnswer { JavaOnlyArray() } + arguments.`when`(Arguments::createMap).thenAnswer { JavaOnlyMap() } + + requestBodyUtil = mockStatic(RequestBodyUtil::class.java) + requestBodyUtil + .`when` { + RequestBodyUtil.getFileInputStream(any(ReactContext::class.java), any(String::class.java)) + } + .thenReturn(mock(InputStream::class.java)) + + requestBodyUtil + .`when` { + RequestBodyUtil.create(any(MediaType::class.java), any(InputStream::class.java)) + } + .thenReturn(mock(RequestBody::class.java)) + + requestBodyUtil + .`when` { + RequestBodyUtil.createProgressRequest( + any(RequestBody::class.java), any(ProgressListener::class.java)) + } + .thenCallRealMethod() + + requestArgumentCaptor = ArgumentCaptor.forClass(Request::class.java) + } + + @After + fun tearDown() { + arguments.close() + requestBodyUtil.close() + } + + @Test + fun testGetWithoutHeaders() { + networkingModule.sendRequest( + "GET", + "http://somedomain/foo", + 0.0, /* requestId */ + JavaOnlyArray.of(), /* headers */ + null, /* body */ + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + verify(httpClient).newCall(capture(requestArgumentCaptor)) + assertThat(requestArgumentCaptor.value.url().toString()).isEqualTo("http://somedomain/foo") + // We set the User-Agent header by default + assertThat(requestArgumentCaptor.value.headers().size()).isEqualTo(1) + assertThat(requestArgumentCaptor.value.method()).isEqualTo("GET") + } + + @Test + fun testFailGetWithInvalidHeadersStruct() { + val invalidHeaders = listOf(JavaOnlyArray.of("foo")) + + networkingModule.sendRequest( + "GET", + "http://somedoman/foo", + 0.0, /* requestId */ + JavaOnlyArray.from(invalidHeaders), /* headers */ + null, /* body */ + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + verifyErrorEmit(context, 0) + } + + @Test + fun testFailPostWithoutContentType() { + val body = JavaOnlyMap() + body.putString("string", "This is request body") + + networkingModule.sendRequest( + "POST", + "http://somedomain/bar", + 0.0, + JavaOnlyArray.of(), + body, + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + verifyErrorEmit(context, 0) + } + + @Test + fun testFailInvalidUrl() { + networkingModule.sendRequest( + "GET", + "aaa", + 0.0, /* requestId */ + JavaOnlyArray.of(), /* headers */ + null, /* body */ + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + verifyErrorEmit(context, 0) + } + + private fun verifyErrorEmit(context: ReactApplicationContext, requestId: Int) { + val captor = ArgumentCaptor.forClass(WritableArray::class.java) + verify(context).emitDeviceEvent(eq("didCompleteNetworkResponse"), captor.capture()) + + val array = captor.value + assertThat(array.getInt(0)).isEqualTo(requestId) + assertThat(array.getString(1)).isNotNull + } + + @Test + fun testSuccessfulPostRequest() { + val body = JavaOnlyMap() + body.putString("string", "This is request body") + + networkingModule.sendRequest( + "POST", + "http://somedomain/bar", + 0.0, + JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "text/plain")), + body, + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + verify(httpClient).newCall(capture(requestArgumentCaptor)) + assertThat(requestArgumentCaptor.value.url().toString()).isEqualTo("http://somedomain/bar") + assertThat(requestArgumentCaptor.value.headers().size()).isEqualTo(2) + assertThat(requestArgumentCaptor.value.method()).isEqualTo("POST") + assertThat(requestArgumentCaptor.value.body()!!.contentType()!!.type()).isEqualTo("text") + assertThat(requestArgumentCaptor.value.body()!!.contentType()!!.subtype()).isEqualTo("plain") + val contentBuffer = Buffer() + requestArgumentCaptor.value.body()!!.writeTo(contentBuffer) + assertThat(contentBuffer.readUtf8()).isEqualTo("This is request body") + } + + @Test + fun testHeaders() { + val headers = + listOf( + JavaOnlyArray.of("Accept", "text/plain"), + JavaOnlyArray.of("User-Agent", "React test agent/1.0")) + + networkingModule.sendRequest( + "GET", + "http://someurl/baz", + 0.0, + JavaOnlyArray.from(headers), + null, + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + verify(httpClient).newCall(capture(requestArgumentCaptor)) + val requestHeaders = requestArgumentCaptor.value.headers() + assertThat(requestHeaders.size()).isEqualTo(2) + assertThat(requestHeaders["Accept"]).isEqualTo("text/plain") + assertThat(requestHeaders["User-Agent"]).isEqualTo("React test agent/1.0") + } + + @Test + fun testPostJsonContentTypeHeader() { + val body = JavaOnlyMap() + body.putString("string", "{ \"key\": \"value\" }") + + networkingModule.sendRequest( + "POST", + "http://somedomain/bar", + 0.0, + JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "application/json")), + body, + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + verify(httpClient).newCall(capture(requestArgumentCaptor)) + + // Verify okhttp does not append "charset=utf-8" + assertThat(requestArgumentCaptor.value.body()!!.contentType().toString()) + .isEqualTo("application/json") + } + + @Test + fun testRespectsExistingCharacterSet() { + val testString = "Friðjónsson" + + val body = JavaOnlyMap() + body.putString("string", testString) + + networkingModule.sendRequest( + "POST", + "http://somedomain/bar", + 0.0, + JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "text/plain; charset=utf-16")), + body, + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + verify(httpClient).newCall(capture(requestArgumentCaptor)) + + val contentBuffer = Buffer() + requestArgumentCaptor.value.body()!!.writeTo(contentBuffer) + assertThat(contentBuffer.readString(StandardCharsets.UTF_16)).isEqualTo(testString) + } + + @Test + fun testGracefullyRecoversFromInvalidContentType() { + val body = JavaOnlyMap() + body.putString("string", "test") + + networkingModule.sendRequest( + "POST", + "http://somedomain/bar", + 0.0, + JavaOnlyArray.of(JavaOnlyArray.of("Content-Type", "invalid")), + body, + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + verify(httpClient).newCall(capture(requestArgumentCaptor)) + + val contentBuffer = Buffer() + requestArgumentCaptor.value.body()!!.writeTo(contentBuffer) + + assertThat(contentBuffer.readString(StandardCharsets.UTF_8)).isEqualTo("test") + assertThat(requestArgumentCaptor.value.header("Content-Type")).isEqualTo("invalid") + } + + @Test + fun testMultipartPostRequestSimple() { + val body = JavaOnlyMap() + val formData = JavaOnlyArray() + val bodyPart = JavaOnlyMap() + bodyPart.putString("string", "value") + bodyPart.putArray( + "headers", JavaOnlyArray.from(listOf(JavaOnlyArray.of("content-disposition", "name")))) + + formData.pushMap(bodyPart) + body.putArray("formData", formData) + + networkingModule.sendRequest( + "POST", + "http://someurl/uploadFoo", + 0.0, + JavaOnlyArray(), + body, + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + // verify url, method, headers + verify(httpClient).newCall(capture(requestArgumentCaptor)) + assertThat(requestArgumentCaptor.value.url().toString()).isEqualTo("http://someurl/uploadFoo") + assertThat(requestArgumentCaptor.value.method()).isEqualTo("POST") + assertThat(requestArgumentCaptor.value.body()!!.contentType()!!.type()) + .isEqualTo(MultipartBody.FORM.type()) + assertThat(requestArgumentCaptor.value.body()!!.contentType()!!.subtype()) + .isEqualTo(MultipartBody.FORM.subtype()) + + val requestHeaders = requestArgumentCaptor.value.headers() + assertThat(requestHeaders.size()).isEqualTo(1) + } + + @Test + fun testMultipartPostRequestHeaders() { + val headers = + listOf( + JavaOnlyArray.of("Accept", "text/plain"), + JavaOnlyArray.of("User-Agent", "React test agent/1.0"), + JavaOnlyArray.of("content-type", "multipart/form-data")) + + val body = JavaOnlyMap() + val formData = JavaOnlyArray() + val bodyPart = JavaOnlyMap() + bodyPart.putString("string", "value") + bodyPart.putArray( + "headers", JavaOnlyArray.from(listOf(JavaOnlyArray.of("content-disposition", "name")))) + + formData.pushMap(bodyPart) + body.putArray("formData", formData) + + networkingModule.sendRequest( + "POST", + "http://someurl/uploadFoo", + 0.0, + JavaOnlyArray.from(headers), + body, + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + // verify url, method, headers + verify(httpClient).newCall(capture(requestArgumentCaptor)) + assertThat(requestArgumentCaptor.value.url().toString()).isEqualTo("http://someurl/uploadFoo") + assertThat(requestArgumentCaptor.value.method()).isEqualTo("POST") + assertThat(requestArgumentCaptor.value.body()!!.contentType()!!.type()) + .isEqualTo(MultipartBody.FORM.type()) + assertThat(requestArgumentCaptor.value.body()!!.contentType()!!.subtype()) + .isEqualTo(MultipartBody.FORM.subtype()) + + val requestHeaders = requestArgumentCaptor.value.headers() + assertThat(requestHeaders.size()).isEqualTo(3) + assertThat(requestHeaders["Accept"]).isEqualTo("text/plain") + assertThat(requestHeaders["User-Agent"]).isEqualTo("React test agent/1.0") + assertThat(requestHeaders["content-type"]).isEqualTo("multipart/form-data") + } + + @Test + @Ignore("TODO: Fix me (T171890419)") + fun testMultipartPostRequestBody() { + val inputStream = mock(InputStream::class.java) + whenever(inputStream.available()).thenReturn("imageUri".length) + + val multipartBuilder = mock(MultipartBody.Builder::class.java) + // TODO This PowerMock statement should be migrated to an equivalent for Mockito + // once this test is unsuppressed. + // whenNew(MultipartBody.Builder.class) + // .withNoArguments() + // .thenReturn(multipartBuilder); + whenever(multipartBuilder.setType(anyOrNull(MediaType::class.java))).thenAnswer { + multipartBuilder + } + whenever(multipartBuilder.addPart(any(Headers::class.java), anyOrNull(RequestBody::class.java))) + .thenAnswer { multipartBuilder } + + whenever(multipartBuilder.build()).thenAnswer { mock(MultipartBody::class.java) } + + val headers = listOf(JavaOnlyArray.of("content-type", "multipart/form-data")) + + val body = JavaOnlyMap() + val formData = JavaOnlyArray() + body.putArray("formData", formData) + + val bodyPart = JavaOnlyMap() + bodyPart.putString("string", "locale") + bodyPart.putArray( + "headers", JavaOnlyArray.from(listOf(JavaOnlyArray.of("content-disposition", "user")))) + + formData.pushMap(bodyPart) + + val imageBodyPart = JavaOnlyMap() + imageBodyPart.putString("uri", "imageUri") + imageBodyPart.putArray( + "headers", + JavaOnlyArray.from( + listOf( + JavaOnlyArray.of("content-type", "image/jpg"), + JavaOnlyArray.of( + "content-disposition", + "filename=\"测试photo.jpg\"; filename*=utf-8''%E6%B5%8B%E8%AF%95photo.jpg")))) + + formData.pushMap(imageBodyPart) + + networkingModule.sendRequest( + "POST", + "http://someurl/uploadFoo", + 0.0, + JavaOnlyArray.from(headers), + body, + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + + // verify RequestBodyPart for image + + // TODO This should be migrated to requestBodyUtil.verify(); + // PowerMockito.verifyStatic(RequestBodyUtil.class, times(1)); + RequestBodyUtil.getFileInputStream(any(ReactContext::class.java), eq("imageUri")) + // TODO This should be migrated to requestBodyUtil.verify(); + // PowerMockito.verifyStatic(RequestBodyUtil.class, times(1)); + RequestBodyUtil.create(MediaType.parse("image/jpg"), inputStream) + + // verify body + // TODO fix it (now mock is not called) + verify(multipartBuilder).build() + verify(multipartBuilder).setType(MultipartBody.FORM) + // TODO fix it (Captors are nulls) + val headersArgumentCaptor = ArgumentCaptor.forClass(Headers::class.java) + val bodyArgumentCaptor = ArgumentCaptor.forClass(RequestBody::class.java) + verify(multipartBuilder, times(2)) + .addPart(capture(headersArgumentCaptor), capture(bodyArgumentCaptor)) + + val bodyHeaders = headersArgumentCaptor.allValues + assertThat(bodyHeaders.size).isEqualTo(2) + val bodyRequestBody = bodyArgumentCaptor.allValues + assertThat(bodyRequestBody.size).isEqualTo(2) + + assertThat(bodyHeaders[0]["content-disposition"]).isEqualTo("user") + assertThat(bodyRequestBody[0].contentType()).isNull() + assertThat(bodyRequestBody[0].contentLength()).isEqualTo("locale".toByteArray().size.toLong()) + + assertThat(bodyHeaders[1]["content-disposition"]) + .isEqualTo("filename=\"测试photo.jpg\"; filename*=utf-8''%E6%B5%8B%E8%AF%95photo.jpg") + + assertThat(bodyRequestBody[1].contentType()).isEqualTo(MediaType.parse("image/jpg")) + assertThat(bodyRequestBody[1].contentLength()).isEqualTo("imageUri".toByteArray().size.toLong()) + } + + @Test + @Ignore("TODO: Fix me (T171890419)") + fun testCancelAllCallsInvalidate() { + val requests = 3 + val calls = arrayOfNulls(requests) + for (idx in 0 until requests) { + calls[idx] = mock(Call::class.java) + } + + whenever(httpClient.newCall(anyOrNull(Request::class.java))).thenAnswer { invocation -> + val request = invocation.arguments[0] as Request + calls[(request.tag() as Int?)!! - 1] + } + networkingModule.initialize() + + for (idx in 0 until requests) { + networkingModule.sendRequest( + "GET", + "http://somedomain/foo", + (idx + 1).toDouble(), + JavaOnlyArray.of(), + null, + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + } + + verify(httpClient, times(3)).newCall(anyOrNull(Request::class.java)) + + networkingModule.invalidate() + // TODO This should be migrated to okHttpCallUtil.verify(); + // PowerMockito.verifyStatic(OkHttpCallUtil.class, times(3)); + val clientArguments = ArgumentCaptor.forClass(OkHttpClient::class.java) + val requestIdArguments = ArgumentCaptor.forClass(Int::class.java) + + OkHttpCallUtil.cancelTag(capture(clientArguments), capture(requestIdArguments)) + + assertThat(requestIdArguments.allValues.size).isEqualTo(requests) + for (idx in 0 until requests) { + assertThat(requestIdArguments.allValues.contains(idx + 1)).isTrue + } + } + + @Test + @Ignore("TODO: Fix me (T171890419)") + fun testCancelSomeCallsInvalidate() { + val requests = 3 + val calls = arrayOfNulls(requests) + for (idx in 0 until requests) { + calls[idx] = mock(Call::class.java) + } + whenever(httpClient.newCall(anyOrNull(Request::class.java))).thenAnswer { invocation -> + val request = invocation.arguments[0] as Request + calls[(request.tag() as Int?)!! - 1] + } + + for (idx in 0 until requests) { + networkingModule.sendRequest( + "GET", + "http://somedomain/foo", + (idx + 1).toDouble(), + JavaOnlyArray.of(), + null, + "text", /* responseType */ + true, /* useIncrementalUpdates*/ + 0.0, /* timeout */ + false /* withCredentials */) + } + verify(httpClient, times(3)).newCall(anyOrNull(Request::class.java)) + + networkingModule.abortRequest(requests.toDouble()) + // TODO This should be migrated to okHttpCallUtil.verify(); + // PowerMockito.verifyStatic(OkHttpCallUtil.class, times(1)); + var clientArguments = ArgumentCaptor.forClass(OkHttpClient::class.java) + var requestIdArguments = ArgumentCaptor.forClass(Int::class.java) + OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()) + println(requestIdArguments.allValues) + assertThat(requestIdArguments.allValues.size).isEqualTo(1) + assertThat(requestIdArguments.allValues[0]).isEqualTo(requests) + + // verifyStatic actually does not clear all calls so far, so we have to check for all of + // them. + // If `cancelTag` would've been called again for the aborted call, we would have had + // `requests + 1` calls. + networkingModule.invalidate() + // TODO This should be migrated to okHttpCallUtil.verify(); + // PowerMockito.verifyStatic(OkHttpCallUtil.class, times(requests)); + clientArguments = ArgumentCaptor.forClass(OkHttpClient::class.java) + requestIdArguments = ArgumentCaptor.forClass(Int::class.java) + OkHttpCallUtil.cancelTag(clientArguments.capture(), requestIdArguments.capture()) + assertThat(requestIdArguments.allValues.size).isEqualTo(requests) + for (idx in 0 until requests) { + assertThat(requestIdArguments.allValues.contains(idx + 1)).isTrue + } + } +}