From c0f60d201839ce8ec572afb4da164c4773fd94e1 Mon Sep 17 00:00:00 2001 From: Konstantin Raev Date: Thu, 26 Nov 2015 07:25:54 -0800 Subject: [PATCH] Moved React Native Android unit tests to open source Reviewed By: mkonicek Differential Revision: D2685799 fb-gh-sync-id: 56f061df58641c8cb13fc16bad5f87039f0c49fb --- .../react/bridge/CatalystTestHelper.java | 64 +++ .../facebook/react/bridge/SimpleArray.java | 145 ++++++ .../com/facebook/react/bridge/SimpleMap.java | 168 +++++++ .../modules/network/NetworkingModuleTest.java | 466 ++++++++++++++++++ .../storage/AsyncStorageModuleTest.java | 352 +++++++++++++ .../modules/timing/TimingModuleTest.java | 180 +++++++ 6 files changed, 1375 insertions(+) create mode 100644 ReactAndroid/src/test/java/com/facebook/react/bridge/CatalystTestHelper.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/bridge/SimpleArray.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/bridge/SimpleMap.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/modules/storage/AsyncStorageModuleTest.java create mode 100644 ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java diff --git a/ReactAndroid/src/test/java/com/facebook/react/bridge/CatalystTestHelper.java b/ReactAndroid/src/test/java/com/facebook/react/bridge/CatalystTestHelper.java new file mode 100644 index 0000000000..27a1b7a9fe --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/bridge/CatalystTestHelper.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge; + +import com.facebook.react.bridge.queue.CatalystQueueConfiguration; +import com.facebook.react.bridge.queue.CatalystQueueConfigurationImpl; +import com.facebook.react.bridge.queue.CatalystQueueConfigurationSpec; +import com.facebook.react.bridge.queue.MessageQueueThreadSpec; +import com.facebook.react.bridge.queue.QueueThreadExceptionHandler; +import com.facebook.react.uimanager.UIManagerModule; + +import org.robolectric.RuntimeEnvironment; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Utility for creating pre-configured instances of core react components for tests. + */ +public class CatalystTestHelper { + + /** + * @return a ReactApplicationContext that has a CatalystInstance mock returned by + * {@link #createMockCatalystInstance} + */ + public static ReactApplicationContext createCatalystContextForTest() { + ReactApplicationContext context = + new ReactApplicationContext(RuntimeEnvironment.application); + context.initializeWithInstance(createMockCatalystInstance()); + return context; + } + + /** + * @return a CatalystInstance mock that has a default working CatalystQueueConfiguration. + */ + public static CatalystInstance createMockCatalystInstance() { + CatalystQueueConfigurationSpec spec = CatalystQueueConfigurationSpec.builder() + .setJSQueueThreadSpec(MessageQueueThreadSpec.mainThreadSpec()) + .setNativeModulesQueueThreadSpec(MessageQueueThreadSpec.mainThreadSpec()) + .build(); + CatalystQueueConfiguration catalystQueueConfiguration = CatalystQueueConfigurationImpl.create( + spec, + new QueueThreadExceptionHandler() { + @Override + public void handleException(Exception e) { + throw new RuntimeException(e); + } + }); + + CatalystInstance reactInstance = mock(CatalystInstance.class); + when(reactInstance.getCatalystQueueConfiguration()).thenReturn(catalystQueueConfiguration); + when(reactInstance.getNativeModule(UIManagerModule.class)) + .thenReturn(mock(UIManagerModule.class)); + + return reactInstance; + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/bridge/SimpleArray.java b/ReactAndroid/src/test/java/com/facebook/react/bridge/SimpleArray.java new file mode 100644 index 0000000000..15656bde42 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/bridge/SimpleArray.java @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A simple read/write array that can be used in tests in place of {@link WritableNativeArray}. + */ +public class SimpleArray implements ReadableArray, WritableArray { + + private final List mBackingList; + + public static SimpleArray from(List list) { + return new SimpleArray(list); + } + + public static SimpleArray of(Object... values) { + return new SimpleArray(values); + } + + private SimpleArray(Object... values) { + mBackingList = Arrays.asList(values); + } + + private SimpleArray(List list) { + mBackingList = new ArrayList(list); + } + + public SimpleArray() { + mBackingList = new ArrayList(); + } + + @Override + public int size() { + return mBackingList.size(); + } + + @Override + public boolean isNull(int index) { + return mBackingList.get(index) == null; + } + + @Override + public double getDouble(int index) { + return (Double) mBackingList.get(index); + } + + @Override + public int getInt(int index) { + return (Integer) mBackingList.get(index); + } + + @Override + public String getString(int index) { + return (String) mBackingList.get(index); + } + + @Override + public SimpleArray getArray(int index) { + return (SimpleArray) mBackingList.get(index); + } + + @Override + public boolean getBoolean(int index) { + return (Boolean) mBackingList.get(index); + } + + @Override + public SimpleMap getMap(int index) { + return (SimpleMap) mBackingList.get(index); + } + + @Override + public ReadableType getType(int index) { + return null; + } + + @Override + public void pushBoolean(boolean value) { + mBackingList.add(value); + } + + @Override + public void pushDouble(double value) { + mBackingList.add(value); + } + + @Override + public void pushInt(int value) { + mBackingList.add(value); + } + + @Override + public void pushString(String value) { + mBackingList.add(value); + } + + @Override + public void pushArray(WritableArray array) { + mBackingList.add(array); + } + + @Override + public void pushMap(WritableMap map) { + mBackingList.add(map); + } + + @Override + public void pushNull() { + mBackingList.add(null); + } + + @Override + public String toString() { + return mBackingList.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SimpleArray that = (SimpleArray) o; + + if (mBackingList != null ? !mBackingList.equals(that.mBackingList) : that.mBackingList != null) + return false; + + return true; + } + + @Override + public int hashCode() { + return mBackingList != null ? mBackingList.hashCode() : 0; + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/bridge/SimpleMap.java b/ReactAndroid/src/test/java/com/facebook/react/bridge/SimpleMap.java new file mode 100644 index 0000000000..f86f04e8cc --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/bridge/SimpleMap.java @@ -0,0 +1,168 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * A simple read/write map that can be used in tests in place of {@link WritableNativeMap}. + */ +public class SimpleMap implements ReadableMap, WritableMap { + + private final Map mBackingMap; + + public static SimpleMap of(Object... keysAndValues) { + return new SimpleMap(keysAndValues); + } + + /** + * @param keysAndValues keys and values, interleaved + */ + private SimpleMap(Object... keysAndValues) { + if (keysAndValues.length % 2 != 0) { + throw new IllegalArgumentException("You must provide the same number of keys and values"); + } + mBackingMap = new HashMap(); + for (int i = 0; i < keysAndValues.length; i += 2) { + mBackingMap.put(keysAndValues[i], keysAndValues[i + 1]); + } + } + + public SimpleMap() { + mBackingMap = new HashMap(); + } + + @Override + public boolean hasKey(String name) { + return mBackingMap.containsKey(name); + } + + @Override + public boolean isNull(String name) { + return mBackingMap.get(name) == null; + } + + @Override + public boolean getBoolean(String name) { + return (Boolean) mBackingMap.get(name); + } + + @Override + public double getDouble(String name) { + return (Double) mBackingMap.get(name); + } + + @Override + public int getInt(String name) { + return (Integer) mBackingMap.get(name); + } + + @Override + public String getString(String name) { + return (String) mBackingMap.get(name); + } + + @Override + public SimpleMap getMap(String name) { + return (SimpleMap) mBackingMap.get(name); + } + + @Override + public SimpleArray getArray(String name) { + return (SimpleArray) mBackingMap.get(name); + } + + @Override + public ReadableType getType(String name) { + throw new UnsupportedOperationException("Method not implemented"); + } + + @Override + public ReadableMapKeySetIterator keySetIterator() { + return new ReadableMapKeySetIterator() { + Iterator mIterator = mBackingMap.keySet().iterator(); + + @Override + public boolean hasNextKey() { + return mIterator.hasNext(); + } + + @Override + public String nextKey() { + return mIterator.next(); + } + }; + } + + @Override + public void putBoolean(String key, boolean value) { + mBackingMap.put(key, value); + } + + @Override + public void putDouble(String key, double value) { + mBackingMap.put(key, value); + } + + @Override + public void putInt(String key, int value) { + mBackingMap.put(key, value); + } + + @Override + public void putString(String key, String value) { + mBackingMap.put(key, value); + } + + @Override + public void putNull(String key) { + mBackingMap.put(key, null); + } + + @Override + public void putMap(String key, WritableMap value) { + mBackingMap.put(key, value); + } + + @Override + public void merge(ReadableMap source) { + mBackingMap.putAll(((SimpleMap) source).mBackingMap); + } + + @Override + public void putArray(String key, WritableArray value) { + mBackingMap.put(key, value); + } + + @Override + public String toString() { + return mBackingMap.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SimpleMap that = (SimpleMap) o; + + if (mBackingMap != null ? !mBackingMap.equals(that.mBackingMap) : that.mBackingMap != null) + return false; + + return true; + } + + @Override + public int hashCode() { + return mBackingMap != null ? mBackingMap.hashCode() : 0; + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java new file mode 100644 index 0000000000..454a3cb986 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/network/NetworkingModuleTest.java @@ -0,0 +1,466 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.network; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.SimpleArray; +import com.facebook.react.bridge.SimpleMap; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter; + +import com.squareup.okhttp.Call; +import com.squareup.okhttp.Headers; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.MultipartBuilder; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.RequestBody; +import okio.Buffer; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.fest.assertions.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.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link NetworkingModule}. + */ +@PrepareForTest({ + Arguments.class, + Call.class, + RequestBodyUtil.class, + MultipartBuilder.class, + NetworkingModule.class, + OkHttpClient.class}) +@RunWith(RobolectricTestRunner.class) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +public class NetworkingModuleTest { + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Test + public void testGetWithoutHeaders() throws Exception { + OkHttpClient httpClient = mock(OkHttpClient.class); + when(httpClient.newCall(any(Request.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Call callMock = mock(Call.class); + return callMock; + } + }); + + NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + + networkingModule.sendRequest( + "GET", + "http://somedomain/foo", + 0, + SimpleArray.of(), + null, + true); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); + verify(httpClient).newCall(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().urlString()).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 { + RCTDeviceEventEmitter emitter = mock(RCTDeviceEventEmitter.class); + ReactApplicationContext context = mock(ReactApplicationContext.class); + when(context.getJSModule(any(Class.class))).thenReturn(emitter); + + OkHttpClient httpClient = mock(OkHttpClient.class); + NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient); + + List invalidHeaders = Arrays.asList(SimpleArray.of("foo")); + + mockEvents(); + + networkingModule.sendRequest( + "GET", + "http://somedoman/foo", + 0, + SimpleArray.from(invalidHeaders), + null, + true); + + verifyErrorEmit(emitter, 0); + } + + @Test + public void testFailPostWithoutContentType() throws Exception { + RCTDeviceEventEmitter emitter = mock(RCTDeviceEventEmitter.class); + ReactApplicationContext context = mock(ReactApplicationContext.class); + when(context.getJSModule(any(Class.class))).thenReturn(emitter); + + OkHttpClient httpClient = mock(OkHttpClient.class); + NetworkingModule networkingModule = new NetworkingModule(context, "", httpClient); + + SimpleMap body = new SimpleMap(); + body.putString("string", "This is request body"); + + mockEvents(); + + networkingModule.sendRequest( + "POST", + "http://somedomain/bar", + 0, + SimpleArray.of(), + body, + true); + + verifyErrorEmit(emitter, 0); + } + + private static void verifyErrorEmit(RCTDeviceEventEmitter emitter, int requestId) { + ArgumentCaptor captor = ArgumentCaptor.forClass(WritableArray.class); + verify(emitter).emit(eq("didCompleteNetworkResponse"), captor.capture()); + + WritableArray array = captor.getValue(); + assertThat(array.getInt(0)).isEqualTo(requestId); + assertThat(array.getString(1)).isNotNull(); + } + + private static void mockEvents() { + PowerMockito.mockStatic(Arguments.class); + Mockito.when(Arguments.createArray()).thenAnswer( + new Answer() { + @Override + public WritableArray answer(InvocationOnMock invocation) throws Throwable { + return new SimpleArray(); + } + }); + + Mockito.when(Arguments.createMap()).thenAnswer( + new Answer() { + @Override + public WritableMap answer(InvocationOnMock invocation) throws Throwable { + return new SimpleMap(); + } + }); + } + + @Test + public void testSuccessfullPostRequest() throws Exception { + OkHttpClient httpClient = mock(OkHttpClient.class); + when(httpClient.newCall(any(Request.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Call callMock = mock(Call.class); + return callMock; + } + }); + + NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + + SimpleMap body = new SimpleMap(); + body.putString("string", "This is request body"); + + networkingModule.sendRequest( + "POST", + "http://somedomain/bar", + 0, + SimpleArray.of(SimpleArray.of("Content-Type", "text/plain")), + body, + true); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); + verify(httpClient).newCall(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().urlString()).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 { + OkHttpClient httpClient = mock(OkHttpClient.class); + when(httpClient.newCall(any(Request.class))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Call callMock = mock(Call.class); + return callMock; + } + }); + NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + + List headers = Arrays.asList( + SimpleArray.of("Accept", "text/plain"), + SimpleArray.of("User-Agent", "React test agent/1.0")); + + networkingModule.sendRequest( + "GET", + "http://someurl/baz", + 0, + SimpleArray.from(headers), + null, + true); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); + verify(httpClient).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 testMultipartPostRequestSimple() throws Exception { + PowerMockito.mockStatic(RequestBodyUtil.class); + when(RequestBodyUtil.getFileInputStream(any(ReactContext.class), any(String.class))) + .thenReturn(mock(InputStream.class)); + when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))) + .thenReturn(mock(RequestBody.class)); + + SimpleMap body = new SimpleMap(); + SimpleArray formData = new SimpleArray(); + SimpleMap bodyPart = new SimpleMap(); + bodyPart.putString("string", "value"); + bodyPart.putArray( + "headers", + SimpleArray.from( + Arrays.asList( + SimpleArray.of("content-disposition", "name")))); + formData.pushMap(bodyPart); + body.putArray("formData", formData); + + OkHttpClient httpClient = mock(OkHttpClient.class); + when(httpClient.newCall(any(Request.class))).thenAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Call callMock = mock(Call.class); + return callMock; + } + }); + + NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + networkingModule.sendRequest( + "POST", + "http://someurl/uploadFoo", + 0, + new SimpleArray(), + body, + true); + + // verify url, method, headers + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); + verify(httpClient).newCall(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().urlString()).isEqualTo("http://someurl/uploadFoo"); + assertThat(argumentCaptor.getValue().method()).isEqualTo("POST"); + assertThat(argumentCaptor.getValue().body().contentType().type()). + isEqualTo(MultipartBuilder.FORM.type()); + assertThat(argumentCaptor.getValue().body().contentType().subtype()). + isEqualTo(MultipartBuilder.FORM.subtype()); + Headers requestHeaders = argumentCaptor.getValue().headers(); + assertThat(requestHeaders.size()).isEqualTo(1); + } + + @Test + public void testMultipartPostRequestHeaders() throws Exception { + PowerMockito.mockStatic(RequestBodyUtil.class); + when(RequestBodyUtil.getFileInputStream(any(ReactContext.class), any(String.class))) + .thenReturn(mock(InputStream.class)); + when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))) + .thenReturn(mock(RequestBody.class)); + + List headers = Arrays.asList( + SimpleArray.of("Accept", "text/plain"), + SimpleArray.of("User-Agent", "React test agent/1.0"), + SimpleArray.of("content-type", "multipart/form-data")); + + SimpleMap body = new SimpleMap(); + SimpleArray formData = new SimpleArray(); + SimpleMap bodyPart = new SimpleMap(); + bodyPart.putString("string", "value"); + bodyPart.putArray( + "headers", + SimpleArray.from( + Arrays.asList( + SimpleArray.of("content-disposition", "name")))); + formData.pushMap(bodyPart); + body.putArray("formData", formData); + + OkHttpClient httpClient = mock(OkHttpClient.class); + when(httpClient.newCall(any(Request.class))).thenAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Call callMock = mock(Call.class); + return callMock; + } + }); + + NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + networkingModule.sendRequest( + "POST", + "http://someurl/uploadFoo", + 0, + SimpleArray.from(headers), + body, + true); + + // verify url, method, headers + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Request.class); + verify(httpClient).newCall(argumentCaptor.capture()); + assertThat(argumentCaptor.getValue().urlString()).isEqualTo("http://someurl/uploadFoo"); + assertThat(argumentCaptor.getValue().method()).isEqualTo("POST"); + assertThat(argumentCaptor.getValue().body().contentType().type()). + isEqualTo(MultipartBuilder.FORM.type()); + assertThat(argumentCaptor.getValue().body().contentType().subtype()). + isEqualTo(MultipartBuilder.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); + PowerMockito.mockStatic(RequestBodyUtil.class); + when(RequestBodyUtil.getFileInputStream(any(ReactContext.class), any(String.class))) + .thenReturn(inputStream); + when(RequestBodyUtil.create(any(MediaType.class), any(InputStream.class))).thenCallRealMethod(); + when(inputStream.available()).thenReturn("imageUri".length()); + + final MultipartBuilder multipartBuilder = mock(MultipartBuilder.class); + PowerMockito.whenNew(MultipartBuilder.class).withNoArguments().thenReturn(multipartBuilder); + when(multipartBuilder.type(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(RequestBody.class); + } + }); + + List headers = Arrays.asList( + SimpleArray.of("content-type", "multipart/form-data")); + + SimpleMap body = new SimpleMap(); + SimpleArray formData = new SimpleArray(); + body.putArray("formData", formData); + + SimpleMap bodyPart = new SimpleMap(); + bodyPart.putString("string", "locale"); + bodyPart.putArray( + "headers", + SimpleArray.from( + Arrays.asList( + SimpleArray.of("content-disposition", "user")))); + formData.pushMap(bodyPart); + + SimpleMap imageBodyPart = new SimpleMap(); + imageBodyPart.putString("uri", "imageUri"); + imageBodyPart.putArray( + "headers", + SimpleArray.from( + Arrays.asList( + SimpleArray.of("content-type", "image/jpg"), + SimpleArray.of("content-disposition", "filename=photo.jpg")))); + formData.pushMap(imageBodyPart); + + OkHttpClient httpClient = mock(OkHttpClient.class); + when(httpClient.newCall(any(Request.class))).thenAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Call callMock = mock(Call.class); + return callMock; + } + }); + + NetworkingModule networkingModule = new NetworkingModule(null, "", httpClient); + networkingModule.sendRequest( + "POST", + "http://someurl/uploadFoo", + 0, + SimpleArray.from(headers), + body, + true); + + // verify RequestBodyPart for image + PowerMockito.verifyStatic(times(1)); + RequestBodyUtil.getFileInputStream(any(ReactContext.class), eq("imageUri")); + PowerMockito.verifyStatic(times(1)); + RequestBodyUtil.create(MediaType.parse("image/jpg"), inputStream); + + // verify body + verify(multipartBuilder).build(); + verify(multipartBuilder).type(MultipartBuilder.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"); + assertThat(bodyRequestBody.get(1).contentType()).isEqualTo(MediaType.parse("image/jpg")); + assertThat(bodyRequestBody.get(1).contentLength()).isEqualTo("imageUri".getBytes().length); + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/storage/AsyncStorageModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/storage/AsyncStorageModuleTest.java new file mode 100644 index 0000000000..bd7076fc92 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/storage/AsyncStorageModuleTest.java @@ -0,0 +1,352 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.storage; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.SimpleArray; +import com.facebook.react.bridge.SimpleMap; +import com.facebook.react.modules.storage.AsyncStorageModule; +import com.facebook.react.modules.storage.ReactDatabaseSupplier; + + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.mockito.Mockito.mock; +import static org.fest.assertions.api.Assertions.assertThat; + +/** + * Tests for {@link AsyncStorageModule}. + */ +@PrepareForTest({Arguments.class}) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +@RunWith(RobolectricTestRunner.class) +public class AsyncStorageModuleTest { + + private AsyncStorageModule mStorage; + private SimpleArray mEmptyArray; + + private static class FakeFragmentContext extends ContextWrapper { + + public FakeFragmentContext(Context base) { + super(base); + } + } + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Before + public void prepareModules() { + PowerMockito.mockStatic(Arguments.class); + Mockito.when(Arguments.createArray()).thenAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new SimpleArray(); + } + }); + + Mockito.when(Arguments.createMap()).thenAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new SimpleMap(); + } + }); + + // don't use Robolectric before initializing mocks + mStorage = new AsyncStorageModule(new ReactApplicationContext( + new FakeFragmentContext(RuntimeEnvironment.application))); + mEmptyArray = new SimpleArray(); + } + + @After + public void cleanUp() { + RuntimeEnvironment.application.deleteDatabase(ReactDatabaseSupplier.DATABASE_NAME); + ReactDatabaseSupplier.deleteInstance(); + } + + @Test + public void testMultiSetMultiGet() { + final String key1 = "foo1"; + final String key2 = "foo2"; + final String fakeKey = "fakeKey"; + final String value1 = "bar1"; + final String value2 = "bar2"; + SimpleArray keyValues = new SimpleArray(); + keyValues.pushArray(getArray(key1, value1)); + keyValues.pushArray(getArray(key2, value2)); + + Callback setCallback = mock(Callback.class); + mStorage.multiSet(keyValues, setCallback); + Mockito.verify(setCallback, Mockito.times(1)).invoke(); + + SimpleArray keys = new SimpleArray(); + keys.pushString(key1); + keys.pushString(key2); + + Callback getCallback = mock(Callback.class); + mStorage.multiGet(keys, getCallback); + Mockito.verify(getCallback, Mockito.times(1)).invoke(null, keyValues); + + keys.pushString(fakeKey); + SimpleArray row3 = new SimpleArray(); + row3.pushString(fakeKey); + row3.pushString(null); + keyValues.pushArray(row3); + + Callback getCallback2 = mock(Callback.class); + mStorage.multiGet(keys, getCallback2); + Mockito.verify(getCallback2, Mockito.times(1)).invoke(null, keyValues); + } + + @Test + public void testMultiRemove() { + final String key1 = "foo1"; + final String key2 = "foo2"; + final String value1 = "bar1"; + final String value2 = "bar2"; + + SimpleArray keyValues = new SimpleArray(); + keyValues.pushArray(getArray(key1, value1)); + keyValues.pushArray(getArray(key2, value2)); + mStorage.multiSet(keyValues, mock(Callback.class)); + + SimpleArray keys = new SimpleArray(); + keys.pushString(key1); + keys.pushString(key2); + + Callback getCallback = mock(Callback.class); + mStorage.multiRemove(keys, getCallback); + Mockito.verify(getCallback, Mockito.times(1)).invoke(); + + Callback getAllCallback = mock(Callback.class); + mStorage.getAllKeys(getAllCallback); + Mockito.verify(getAllCallback, Mockito.times(1)).invoke(null, mEmptyArray); + + mStorage.multiSet(keyValues, mock(Callback.class)); + + keys.pushString("fakeKey"); + Callback getCallback2 = mock(Callback.class); + mStorage.multiRemove(keys, getCallback2); + Mockito.verify(getCallback2, Mockito.times(1)).invoke(); + + Callback getAllCallback2 = mock(Callback.class); + mStorage.getAllKeys(getAllCallback2); + Mockito.verify(getAllCallback2, Mockito.times(1)).invoke(null, mEmptyArray); + } + + @Test + public void testMultiMerge() throws Exception { + final String mergeKey = "mergeTest"; + + JSONObject value = new JSONObject(); + value.put("foo1", "bar1"); + value.put("foo2", createJSONArray("val1", "val2", 3)); + value.put("foo3", 1001); + value.put("foo4", createJSONObject("key1", "randomValueThatWillNeverBeUsed")); + + mStorage.multiSet(SimpleArray.of(getArray(mergeKey, value.toString())), mock(Callback.class)); + { + Callback callback = mock(Callback.class); + mStorage.multiGet(getArray(mergeKey), callback); + Mockito.verify(callback, Mockito.times(1)) + .invoke(null, SimpleArray.of(getArray(mergeKey, value.toString()))); + } + + value.put("foo1", 1001); + value.put("foo2", createJSONObject("key1", "val1")); + value.put("foo3", "bar1"); + value.put("foo4", createJSONArray("val1", "val2", 3)); + + JSONObject newValue = new JSONObject(); + newValue.put("foo2", createJSONObject("key2", "val2")); + + JSONObject newValue2 = new JSONObject(); + newValue2.put("foo2", createJSONObject("key1", "val3")); + + mStorage.multiMerge( + SimpleArray.of( + SimpleArray.of(mergeKey, value.toString()), + SimpleArray.of(mergeKey, newValue.toString()), + SimpleArray.of(mergeKey, newValue2.toString())), + mock(Callback.class)); + + value.put("foo2", createJSONObject("key1", "val3", "key2", "val2")); + Callback callback = mock(Callback.class); + mStorage.multiGet(getArray(mergeKey), callback); + Mockito.verify(callback, Mockito.times(1)) + .invoke(null, SimpleArray.of(getArray(mergeKey, value.toString()))); + } + + @Test + public void testGetAllKeys() { + final String[] keys = {"foo", "foo2"}; + final String[] values = {"bar", "bar2"}; + SimpleArray keyValues = new SimpleArray(); + keyValues.pushArray(getArray(keys[0], values[0])); + keyValues.pushArray(getArray(keys[1], values[1])); + mStorage.multiSet(keyValues, mock(Callback.class)); + + SimpleArray storedKeys = new SimpleArray(); + storedKeys.pushString(keys[0]); + storedKeys.pushString(keys[1]); + + Callback getAllCallback = mock(Callback.class); + mStorage.getAllKeys(getAllCallback); + Mockito.verify(getAllCallback, Mockito.times(1)).invoke(null, storedKeys); + + Callback getAllCallback2 = mock(Callback.class); + mStorage.multiRemove(getArray(keys[0]), mock(Callback.class)); + + mStorage.getAllKeys(getAllCallback2); + Mockito.verify(getAllCallback2, Mockito.times(1)).invoke(null, getArray(keys[1])); + + mStorage.multiRemove(getArray(keys[1]), mock(Callback.class)); + Callback getAllCallback3 = mock(Callback.class); + mStorage.getAllKeys(getAllCallback3); + Mockito.verify(getAllCallback3, Mockito.times(1)).invoke(null, mEmptyArray); + } + + @Test + public void testClear() { + SimpleArray keyValues = new SimpleArray(); + keyValues.pushArray(getArray("foo", "foo2")); + keyValues.pushArray(getArray("bar", "bar2")); + mStorage.multiSet(keyValues, mock(Callback.class)); + + Callback clearCallback2 = mock(Callback.class); + mStorage.clear(clearCallback2); + Mockito.verify(clearCallback2, Mockito.times(1)).invoke(); + + Callback getAllCallback2 = mock(Callback.class); + mStorage.getAllKeys(getAllCallback2); + Mockito.verify(getAllCallback2, Mockito.times(1)).invoke(null, mEmptyArray); + } + + @Test + public void testHugeMultiGetMultiGet() { + // Test with many keys, so that it's above the 999 limit per batch imposed by SQLite. + final int keyCount = 1001; + // don't set keys that divide by this magical number, so that we can check that multiGet works, + // and returns null for missing keys + final int magicalNumber = 343; + + SimpleArray keyValues = new SimpleArray(); + for (int i = 0; i < keyCount; i++) { + if (i % magicalNumber > 0) { + keyValues.pushArray(getArray("key" + i, "value" + i)); + } + } + mStorage.multiSet(keyValues, mock(Callback.class)); + SimpleArray keys = new SimpleArray(); + for (int i = 0; i < keyCount; i++) { + keys.pushString("key" + i); + } + mStorage.multiGet( + keys, new Callback() { + @Override + public void invoke(Object... args) { + assertThat(args.length).isEqualTo(2); + SimpleArray resultArray = (SimpleArray) args[1]; + + assertThat(resultArray.size()).isEqualTo(keyCount); + boolean keyReceived[] = new boolean[keyCount]; + for (int i = 0; i < keyCount; i++) { + String key = resultArray.getArray(i).getString(0).substring(3); + int idx = Integer.parseInt(key); + assertThat(keyReceived[idx]).isFalse(); + keyReceived[idx] = true; + + if (idx % magicalNumber > 0) { + String value = resultArray.getArray(i).getString(1).substring(5); + assertThat(key).isEqualTo(value); + } else { + assertThat(resultArray.getArray(i).isNull(1)); + } + } + } + }); + + // Test removal in same test, since it's costly to set up the test again. + // Remove only odd keys + SimpleArray keyRemoves = new SimpleArray(); + for (int i = 0; i < keyCount; i++) { + if (i % 2 > 0) { + keyRemoves.pushString("key" + i); + } + } + mStorage.multiRemove(keyRemoves, mock(Callback.class)); + mStorage.getAllKeys( + new Callback() { + @Override + public void invoke(Object... args) { + SimpleArray resultArray = (SimpleArray) args[1]; + assertThat(resultArray.size()).isEqualTo(499); + for (int i = 0; i < resultArray.size(); i++) { + String key = resultArray.getString(i).substring(3); + int idx = Integer.parseInt(key); + assertThat(idx % 2).isEqualTo(0); + } + } + }); + } + + private static JSONArray createJSONArray(Object... objects) { + return new JSONArray(Arrays.asList(objects)); + } + + private static JSONObject createJSONObject(Object... keysAndValues) { + if (keysAndValues.length % 2 != 0) { + throw new IllegalArgumentException("You must provide the same number of keys and values"); + } + Map map = new HashMap(); + for (int i = 0; i < keysAndValues.length; i += 2) { + map.put(keysAndValues[i], keysAndValues[i + 1]); + } + return new JSONObject(map); + } + + private SimpleArray getArray(String... values) { + SimpleArray array = new SimpleArray(); + for (String value : values) { + array.pushString(value); + } + return array; + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java b/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java new file mode 100644 index 0000000000..0d14cdc43b --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/modules/timing/TimingModuleTest.java @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.modules.timing; + +import android.view.Choreographer; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.CatalystInstance; +import com.facebook.react.bridge.SimpleArray; +import com.facebook.react.uimanager.ReactChoreographer; +import com.facebook.react.common.SystemClock; +import com.facebook.react.modules.core.JSTimersExecution; +import com.facebook.react.modules.core.Timing; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.mockito.Mockito.*; + +/** + * Tests for {@link Timing}. + */ +@PrepareForTest({Arguments.class, SystemClock.class, ReactChoreographer.class}) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +@RunWith(RobolectricTestRunner.class) +public class TimingModuleTest { + + private static final long FRAME_TIME_NS = 17 * 1000 * 1000; // 17 ms + + private Timing mTiming; + private ReactChoreographer mChoreographerMock; + private PostFrameCallbackHandler mPostFrameCallbackHandler; + private long mCurrentTimeNs; + private JSTimersExecution mJSTimersMock; + + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Before + public void prepareModules() { + PowerMockito.mockStatic(Arguments.class); + when(Arguments.createArray()).thenAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return new SimpleArray(); + } + }); + + PowerMockito.mockStatic(SystemClock.class); + when(SystemClock.currentTimeMillis()).thenReturn(mCurrentTimeNs / 1000000); + when(SystemClock.nanoTime()).thenReturn(mCurrentTimeNs); + + mChoreographerMock = mock(ReactChoreographer.class); + PowerMockito.mockStatic(ReactChoreographer.class); + when(ReactChoreographer.getInstance()).thenReturn(mChoreographerMock); + + CatalystInstance catalystInstance = mock(CatalystInstance.class); + ReactApplicationContext reactContext = mock(ReactApplicationContext.class); + when(reactContext.getCatalystInstance()).thenReturn(catalystInstance); + + mCurrentTimeNs = 0; + mPostFrameCallbackHandler = new PostFrameCallbackHandler(); + + doAnswer(mPostFrameCallbackHandler) + .when(mChoreographerMock) + .postFrameCallback( + eq(ReactChoreographer.CallbackType.TIMERS_EVENTS), + any(Choreographer.FrameCallback.class)); + + mTiming = new Timing(reactContext); + mJSTimersMock = mock(JSTimersExecution.class); + when(catalystInstance.getJSModule(JSTimersExecution.class)).thenReturn(mJSTimersMock); + mTiming.initialize(); + } + + private void stepChoreographerFrame() { + Choreographer.FrameCallback callback = mPostFrameCallbackHandler.getAndResetFrameCallback(); + mCurrentTimeNs += FRAME_TIME_NS; + if (callback != null) { + callback.doFrame(mCurrentTimeNs); + } + } + + @Test + public void testSimpleTimer() { + mTiming.onHostResume(); + mTiming.createTimer(1, 0, 0, false); + stepChoreographerFrame(); + verify(mJSTimersMock).callTimers(SimpleArray.of(1)); + reset(mJSTimersMock); + stepChoreographerFrame(); + verifyNoMoreInteractions(mJSTimersMock); + } + + @Test + public void testSimpleRecurringTimer() { + mTiming.createTimer(100, 0, 0, true); + mTiming.onHostResume(); + stepChoreographerFrame(); + verify(mJSTimersMock).callTimers(SimpleArray.of(100)); + + reset(mJSTimersMock); + stepChoreographerFrame(); + verify(mJSTimersMock).callTimers(SimpleArray.of(100)); + } + + @Test + public void testCancelRecurringTimer() { + mTiming.onHostResume(); + mTiming.createTimer(105, 0, 0, true); + + stepChoreographerFrame(); + verify(mJSTimersMock).callTimers(SimpleArray.of(105)); + + reset(mJSTimersMock); + mTiming.deleteTimer(105); + stepChoreographerFrame(); + verifyNoMoreInteractions(mJSTimersMock); + } + + @Test + public void testPausingAndResuming() { + mTiming.onHostResume(); + mTiming.createTimer(41, 0, 0, true); + + stepChoreographerFrame(); + verify(mJSTimersMock).callTimers(SimpleArray.of(41)); + + reset(mJSTimersMock); + mTiming.onHostPause(); + stepChoreographerFrame(); + verifyNoMoreInteractions(mJSTimersMock); + + reset(mJSTimersMock); + mTiming.onHostResume(); + stepChoreographerFrame(); + verify(mJSTimersMock).callTimers(SimpleArray.of(41)); + } + + private static class PostFrameCallbackHandler implements Answer { + + private Choreographer.FrameCallback mFrameCallback; + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + mFrameCallback = (Choreographer.FrameCallback) args[1]; + return null; + } + + public Choreographer.FrameCallback getAndResetFrameCallback() { + Choreographer.FrameCallback callback = mFrameCallback; + mFrameCallback = null; + return callback; + } + } +}