/** * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. * * You are hereby granted a non-exclusive, worldwide, royalty-free license to use, * copy, modify, and distribute this software in source code or binary form for use * in connection with the web services and APIs provided by Facebook. * * As with any software that integrates with the Facebook platform, your use of * this software is subject to the Facebook Developer Principles and Policies * [http://developers.facebook.com/policy/]. This copyright notice shall be * included in all copies or substantial portions of the software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.facebook; import android.app.Activity; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.Color; import android.os.Bundle; import android.os.ConditionVariable; import android.os.Handler; import android.test.ActivityInstrumentationTestCase2; import android.util.Log; import com.facebook.internal.Utility; import junit.framework.AssertionFailedError; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; public class FacebookActivityTestCase<T extends Activity> extends ActivityInstrumentationTestCase2<T> { private static final String TAG = FacebookActivityTestCase.class.getSimpleName(); private static String applicationId; private static String applicationSecret; private static String clientToken; private static TestUserManager testUserManager; public final static String SECOND_TEST_USER_TAG = "Second"; public final static String THIRD_TEST_USER_TAG = "Third"; private TestBlocker testBlocker; protected synchronized TestBlocker getTestBlocker() { if (testBlocker == null) { testBlocker = TestBlocker.createTestBlocker(); } return testBlocker; } public FacebookActivityTestCase(Class<T> activityClass) { super("", activityClass); } protected String[] getDefaultPermissions() { return null; }; protected AccessToken getAccessTokenForSharedUser() { return getAccessTokenForSharedUser(null); } protected AccessToken getAccessTokenForSharedUser(String sessionUniqueUserTag) { return getAccessTokenForSharedUserWithPermissions(sessionUniqueUserTag, getDefaultPermissions()); } protected AccessToken getAccessTokenForSharedUserWithPermissions(String sessionUniqueUserTag, List<String> permissions) { return getTestUserManager().getAccessTokenForSharedUser(permissions, sessionUniqueUserTag); } protected AccessToken getAccessTokenForSharedUserWithPermissions(String sessionUniqueUserTag, String... permissions) { List<String> permissionList = (permissions != null) ? Arrays.asList(permissions) : null; return getAccessTokenForSharedUserWithPermissions(sessionUniqueUserTag, permissionList); } protected TestUserManager getTestUserManager() { if (testUserManager == null) { synchronized (FacebookActivityTestCase.class) { if (testUserManager == null) { readApplicationIdAndSecret(); testUserManager = new TestUserManager(applicationSecret, applicationId); } } } return testUserManager; } // Turns exceptions from the TestBlocker into JUnit assertions protected void waitAndAssertSuccess(TestBlocker testBlocker, int numSignals) { try { testBlocker.waitForSignalsAndAssertSuccess(numSignals); } catch (AssertionFailedError e) { throw e; } catch (Exception e) { fail("Got exception: " + e.getMessage()); } } protected void waitAndAssertSuccess(int numSignals) { waitAndAssertSuccess(getTestBlocker(), numSignals); } protected void waitAndAssertSuccessOrRethrow(int numSignals) throws Exception { getTestBlocker().waitForSignalsAndAssertSuccess(numSignals); } protected void runAndBlockOnUiThread(final int expectedSignals, final Runnable runnable) throws Throwable { final TestBlocker blocker = getTestBlocker(); runTestOnUiThread(new Runnable() { @Override public void run() { runnable.run(); blocker.signal(); } }); // We wait for the operation to complete; wait for as many other signals as we expect. blocker.waitForSignals(1 + expectedSignals); // Wait for the UI thread to become idle so any UI updates the runnable triggered have a chance // to finish before we return. getInstrumentation().waitForIdleSync(); } protected synchronized void readApplicationIdAndSecret() { synchronized (FacebookTestCase.class) { if (applicationId != null && applicationSecret != null && clientToken != null) { return; } AssetManager assets = getInstrumentation().getTargetContext().getResources().getAssets(); InputStream stream = null; final String errorMessage = "could not read applicationId and applicationSecret from config.json; ensure " + "you have run 'configure_unit_tests.sh'. Error: "; try { stream = assets.open("config.json"); String string = Utility.readStreamToString(stream); JSONTokener tokener = new JSONTokener(string); Object obj = tokener.nextValue(); if (!(obj instanceof JSONObject)) { fail(errorMessage + "could not deserialize a JSONObject"); } JSONObject jsonObject = (JSONObject) obj; applicationId = jsonObject.optString("applicationId"); applicationSecret = jsonObject.optString("applicationSecret"); clientToken = jsonObject.optString("clientToken"); if (Utility.isNullOrEmpty(applicationId) || Utility.isNullOrEmpty(applicationSecret) || Utility.isNullOrEmpty(clientToken)) { fail(errorMessage + "config values are missing"); } } catch (IOException e) { fail(errorMessage + e.toString()); } catch (JSONException e) { fail(errorMessage + e.toString()); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { fail(errorMessage + e.toString()); } } } } } protected static String getApplicationId() { return applicationId; } protected static String getApplicationSecret() { return applicationSecret; } protected void setUp() throws Exception { super.setUp(); // Make sure the logging is turned on. FacebookSdk.setIsDebugEnabled(true); // Make sure we have read application ID and secret. readApplicationIdAndSecret(); FacebookSdk.sdkInitialize(getInstrumentation().getTargetContext()); FacebookSdk.setApplicationId(applicationId); FacebookSdk.setClientToken(clientToken); // These are useful for debugging unit test failures. FacebookSdk.addLoggingBehavior(LoggingBehavior.REQUESTS); FacebookSdk.addLoggingBehavior(LoggingBehavior.INCLUDE_ACCESS_TOKENS); // We want the UI thread to be in StrictMode to catch any violations. turnOnStrictModeForUiThread(); // Needed to bypass a dexmaker bug for mockito System.setProperty("dexmaker.dexcache", getInstrumentation().getTargetContext().getCacheDir().getPath()); } protected void tearDown() throws Exception { super.tearDown(); synchronized (this) { if (testBlocker != null) { testBlocker.quit(); } } } protected Bundle getNativeLinkingExtras(String token, String userId) { readApplicationIdAndSecret(); Bundle extras = new Bundle(); String extraLaunchUriString = String .format("fbrpc://facebook/nativethirdparty?app_id=%s&package_name=com.facebook.sdk.tests&class_name=com.facebook.FacebookActivityTests$FacebookTestActivity&access_token=%s", applicationId, token); extras.putString("extra_launch_uri", extraLaunchUriString); extras.putString("expires_in", "3600"); extras.putLong("app_id", Long.parseLong(applicationId)); extras.putString("access_token", token); if(userId != null && !userId.isEmpty()) { extras.putString("user_id", userId); } return extras; } protected JSONObject getAndAssert(AccessToken accessToken, String id) { GraphRequest request = new GraphRequest(accessToken, id); GraphResponse response = request.executeAndWait(); assertNotNull(response); assertNull(response.getError()); JSONObject result = response.getJSONObject(); assertNotNull(result); return result; } protected JSONObject postGetAndAssert(AccessToken accessToken, String path, JSONObject graphObject) { GraphRequest request = GraphRequest.newPostRequest(accessToken, path, graphObject, null); GraphResponse response = request.executeAndWait(); assertNotNull(response); assertNull(response.getError()); JSONObject result = response.getJSONObject(); assertNotNull(result); assertNotNull(result.optString("id")); return getAndAssert(accessToken, result.optString("id")); } protected void setBatchApplicationIdForTestApp() { readApplicationIdAndSecret(); GraphRequest.setDefaultBatchApplicationId(applicationId); } protected JSONObject batchCreateAndGet(AccessToken accessToken, String graphPath, JSONObject graphObject, String fields) { GraphRequest create = GraphRequest.newPostRequest(accessToken, graphPath, graphObject, new ExpectSuccessCallback()); create.setBatchEntryName("create"); GraphRequest get = GraphRequest.newGraphPathRequest(accessToken, "{result=create:$.id}", new ExpectSuccessCallback()); if (fields != null) { Bundle parameters = new Bundle(); parameters.putString("fields", fields); get.setParameters(parameters); } return batchPostAndGet(create, get); } protected JSONObject batchUpdateAndGet(AccessToken accessToken, String graphPath, JSONObject graphObject, String fields) { GraphRequest update = GraphRequest.newPostRequest(accessToken, graphPath, graphObject, new ExpectSuccessCallback()); GraphRequest get = GraphRequest.newGraphPathRequest(accessToken, graphPath, new ExpectSuccessCallback()); if (fields != null) { Bundle parameters = new Bundle(); parameters.putString("fields", fields); get.setParameters(parameters); } return batchPostAndGet(update, get); } protected JSONObject batchPostAndGet(GraphRequest post, GraphRequest get) { List<GraphResponse> responses = GraphRequest.executeBatchAndWait(post, get); assertEquals(2, responses.size()); JSONObject resultGraphObject = responses.get(1).getJSONObject(); assertNotNull(resultGraphObject); return resultGraphObject; } protected JSONObject createStatusUpdate(String unique) { JSONObject statusUpdate = new JSONObject(); String message = String.format( "Check out my awesome new status update posted at: %s. Some chars for you: +\"[]:,%s", new Date(), unique); try { statusUpdate.put("message", message); } catch (JSONException e) { throw new RuntimeException(e); } return statusUpdate; } protected Bitmap createTestBitmap(int size) { Bitmap image = Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565); image.eraseColor(Color.BLUE); return image; } protected void assertDateEqualsWithinDelta(Date expected, Date actual, long deltaInMsec) { long delta = Math.abs(expected.getTime() - actual.getTime()); assertTrue(delta < deltaInMsec); } protected void assertDateDiffersWithinDelta(Date expected, Date actual, long expectedDifference, long deltaInMsec) { long delta = Math.abs(expected.getTime() - actual.getTime()) - expectedDifference; assertTrue(delta < deltaInMsec); } protected void assertNoErrors(List<GraphResponse> responses) { for (int i = 0; i < responses.size(); ++i) { GraphResponse response = responses.get(i); assertNotNull(response); assertNull(response.getError()); } } protected File createTempFileFromAsset(String assetPath) throws IOException { InputStream inputStream = null; FileOutputStream outStream = null; try { AssetManager assets = getActivity().getResources().getAssets(); inputStream = assets.open(assetPath); File outputDir = getActivity().getCacheDir(); // context being the Activity pointer File outputFile = File.createTempFile("prefix", assetPath, outputDir); outStream = new FileOutputStream(outputFile); final int bufferSize = 1024 * 2; byte[] buffer = new byte[bufferSize]; int n = 0; while ((n = inputStream.read(buffer)) != -1) { outStream.write(buffer, 0, n); } return outputFile; } finally { Utility.closeQuietly(outStream); Utility.closeQuietly(inputStream); } } protected void runOnBlockerThread(final Runnable runnable, boolean waitForCompletion) { Runnable runnableToPost = runnable; final ConditionVariable condition = waitForCompletion ? new ConditionVariable(!waitForCompletion) : null; if (waitForCompletion) { runnableToPost = new Runnable() { @Override public void run() { runnable.run(); condition.open(); } }; } TestBlocker blocker = getTestBlocker(); Handler handler = blocker.getHandler(); handler.post(runnableToPost); if (waitForCompletion) { boolean success = condition.block(10000); assertTrue(success); } } protected void closeBlockerAndAssertSuccess() { TestBlocker blocker; synchronized (this) { blocker = getTestBlocker(); testBlocker = null; } blocker.quit(); boolean joined = false; while (!joined) { try { blocker.join(); joined = true; } catch (InterruptedException e) { } } try { blocker.assertSuccess(); } catch (Exception e) { fail(e.toString()); } } protected TestGraphRequestAsyncTask createAsyncTaskOnUiThread(final GraphRequest... requests) throws Throwable { final ArrayList<TestGraphRequestAsyncTask> result = new ArrayList<TestGraphRequestAsyncTask>(); runTestOnUiThread(new Runnable() { @Override public void run() { result.add(new TestGraphRequestAsyncTask(requests)); } }); return result.isEmpty() ? null : result.get(0); } /* * Classes and helpers related to asynchronous requests. */ // A subclass of RequestAsyncTask that knows how to interact with TestBlocker to ensure that tests can wait // on and assert success of async tasks. protected class TestGraphRequestAsyncTask extends GraphRequestAsyncTask { private final TestBlocker blocker = FacebookActivityTestCase.this.getTestBlocker(); public TestGraphRequestAsyncTask(GraphRequest... requests) { super(requests); } public TestGraphRequestAsyncTask(List<GraphRequest> requests) { super(requests); } public TestGraphRequestAsyncTask(GraphRequestBatch requests) { super(requests); } public TestGraphRequestAsyncTask(HttpURLConnection connection, GraphRequest... requests) { super(connection, requests); } public TestGraphRequestAsyncTask(HttpURLConnection connection, List<GraphRequest> requests) { super(connection, requests); } public TestGraphRequestAsyncTask(HttpURLConnection connection, GraphRequestBatch requests) { super(connection, requests); } public final TestBlocker getBlocker() { return blocker; } public final Exception getThrowable() { return getException(); } protected void onPostExecute(List<GraphResponse> result) { try { super.onPostExecute(result); if (getException() != null) { blocker.setException(getException()); } } finally { Log.d("TestRequestAsyncTask", "signaling blocker"); blocker.signal(); } } // In order to be able to block and accumulate exceptions, we want to ensure the async task is really // being started on the blocker's thread, rather than the test's thread. Use this instead of calling // execute directly in unit tests. public void executeOnBlockerThread() { ensureAsyncTaskLoaded(); Runnable runnable = new Runnable() { public void run() { execute(); } }; Handler handler = new Handler(blocker.getLooper()); handler.post(runnable); } private void ensureAsyncTaskLoaded() { // Work around this issue on earlier frameworks: http://stackoverflow.com/a/7818839/782044 try { runAndBlockOnUiThread(0, new Runnable() { @Override public void run() { try { Class.forName("android.os.AsyncTask"); } catch (ClassNotFoundException e) { } } }); } catch (Throwable throwable) { } } } // Provides an implementation of Request.Callback that will assert either success (no error) or failure (error) // of a request, and allow derived classes to perform additional asserts. protected class TestCallback implements GraphRequest.Callback { private final TestBlocker blocker; private final boolean expectSuccess; public TestCallback(TestBlocker blocker, boolean expectSuccess) { this.blocker = blocker; this.expectSuccess = expectSuccess; } public TestCallback(boolean expectSuccess) { this(FacebookActivityTestCase.this.getTestBlocker(), expectSuccess); } @Override public void onCompleted(GraphResponse response) { try { // We expect to be called on the right thread. if (Thread.currentThread() != blocker) { throw new FacebookException("Invalid thread " + Thread.currentThread().getId() + "; expected to be called on thread " + blocker.getId()); } // We expect either success or failure. if (expectSuccess && response.getError() != null) { throw response.getError().getException(); } else if (!expectSuccess && response.getError() == null) { throw new FacebookException("Expected failure case, received no error"); } // Some tests may want more fine-grained control and assert additional conditions. performAsserts(response); } catch (Exception e) { blocker.setException(e); } finally { // Tell anyone waiting on us that this callback was called. blocker.signal(); } } protected void performAsserts(GraphResponse response) { } } // A callback that will assert if the request resulted in an error. protected class ExpectSuccessCallback extends TestCallback { public ExpectSuccessCallback() { super(true); } } // A callback that will assert if the request did NOT result in an error. protected class ExpectFailureCallback extends TestCallback { public ExpectFailureCallback() { super(false); } } public static abstract class MockGraphRequest extends GraphRequest { public abstract GraphResponse createResponse(); } public static class MockGraphRequestBatch extends GraphRequestBatch { public MockGraphRequestBatch(MockGraphRequest... requests) { super(requests); } // Caller must ensure that all the requests in the batch are, in fact, MockRequests. public MockGraphRequestBatch(GraphRequestBatch requests) { super(requests); } @Override List<GraphResponse> executeAndWaitImpl() { List<GraphRequest> requests = getRequests(); List<GraphResponse> responses = new ArrayList<GraphResponse>(); for (GraphRequest request : requests) { MockGraphRequest mockRequest = (MockGraphRequest) request; responses.add(mockRequest.createResponse()); } GraphRequest.runCallbacks(this, responses); return responses; } } private AtomicBoolean strictModeOnForUiThread = new AtomicBoolean(); protected void turnOnStrictModeForUiThread() { // We only ever need to do this once. If the boolean is true, we know that the next runnable // posted to the UI thread will have strict mode on. if (strictModeOnForUiThread.get() == false) { try { runTestOnUiThread(new Runnable() { @Override public void run() { // Double-check whether we really need to still do this on the UI thread. if (strictModeOnForUiThread.compareAndSet(false, true)) { turnOnStrictModeForThisThread(); } } }); } catch (Throwable throwable) { } } } protected void turnOnStrictModeForThisThread() { // We use reflection, because Instrumentation will complain about any references to // StrictMode in API versions < 9 when attempting to run the unit tests. No particular // effort has been made to make this efficient, since we expect to call it just once. try { ClassLoader loader = Thread.currentThread().getContextClassLoader(); Class<?> strictModeClass = Class.forName("android.os.StrictMode", true, loader); Class<?> threadPolicyClass = Class.forName( "android.os.StrictMode$ThreadPolicy", true, loader); Class<?> threadPolicyBuilderClass = Class.forName( "android.os.StrictMode$ThreadPolicy$Builder", true, loader); Object threadPolicyBuilder = threadPolicyBuilderClass.getConstructor().newInstance(); threadPolicyBuilder = threadPolicyBuilderClass.getMethod("detectAll").invoke( threadPolicyBuilder); threadPolicyBuilder = threadPolicyBuilderClass.getMethod("penaltyDeath").invoke( threadPolicyBuilder); Object threadPolicy = threadPolicyBuilderClass.getMethod("build").invoke( threadPolicyBuilder); strictModeClass.getMethod("setThreadPolicy", threadPolicyClass).invoke( strictModeClass, threadPolicy); } catch (Exception ex) { } } }